authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-09-22 12:18:50-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-09-22 12:18:50-07:00
log3fc74135749e32ba106572ed85a2e253b89fb0c6
treefdefb9fa7dc6bda28b9a94ce0c01f711fb06f1ed
parent867e61e10b905cdd1559ed9364ed28decb17d328
parent572956ce240478ed6bdc5d98237e25fea0bab3e5
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #17069 from squeek502/resinator

Add a `.rc` -> `.res` compiler to the Zig compiler

32 files changed, 17043 insertions(+), 13 deletions(-)

lib/std/Build/Step/Compile.zig+65
......@@ -90,6 +90,14 @@ is_linking_libc: bool,
9090is_linking_libcpp: bool,
9191vcpkg_bin_path: ?[]const u8 = null,
9292
93// keep in sync with src/Compilation.zig:RcIncludes
94/// Behavior of automatic detection of include directories when compiling .rc files.
95/// any: Use MSVC if available, fall back to MinGW.
96/// msvc: Use MSVC include paths (must be present on the system).
97/// gnu: Use MinGW include paths (distributed with Zig).
98/// none: Do not use any autodetected include paths.
99rc_includes: enum { any, msvc, gnu, none } = .any,
100
93101installed_path: ?[]const u8,
94102
95103/// Base address for an executable image.
......@@ -221,6 +229,26 @@ pub const CSourceFile = struct {
221229 }
222230};
223231
232pub const RcSourceFile = struct {
233 file: LazyPath,
234 /// Any option that rc.exe accepts will work here, with the exception of:
235 /// - `/fo`: The output filename is set by the build system
236 /// - Any MUI-related option
237 /// https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
238 ///
239 /// Implicitly defined options:
240 /// /x (ignore the INCLUDE environment variable)
241 /// /D_DEBUG or /DNDEBUG depending on the optimization mode
242 flags: []const []const u8 = &.{},
243
244 pub fn dupe(self: RcSourceFile, b: *std.Build) RcSourceFile {
245 return .{
246 .file = self.file.dupe(b),
247 .flags = b.dupeStrings(self.flags),
248 };
249 }
250};
251
224252pub const LinkObject = union(enum) {
225253 static_path: LazyPath,
226254 other_step: *Compile,
......@@ -228,6 +256,7 @@ pub const LinkObject = union(enum) {
228256 assembly_file: LazyPath,
229257 c_source_file: *CSourceFile,
230258 c_source_files: *CSourceFiles,
259 win32_resource_file: *RcSourceFile,
231260};
232261
233262pub const SystemLib = struct {
......@@ -910,6 +939,18 @@ pub fn addCSourceFile(self: *Compile, source: CSourceFile) void {
910939 source.file.addStepDependencies(&self.step);
911940}
912941
942pub fn addWin32ResourceFile(self: *Compile, source: RcSourceFile) void {
943 // Only the PE/COFF format has a Resource Table, so for any other target
944 // the resource file is just ignored.
945 if (self.target.getObjectFormat() != .coff) return;
946
947 const b = self.step.owner;
948 const rc_source_file = b.allocator.create(RcSourceFile) catch @panic("OOM");
949 rc_source_file.* = source.dupe(b);
950 self.link_objects.append(.{ .win32_resource_file = rc_source_file }) catch @panic("OOM");
951 source.file.addStepDependencies(&self.step);
952}
953
913954pub fn setVerboseLink(self: *Compile, value: bool) void {
914955 self.verbose_link = value;
915956}
......@@ -1358,6 +1399,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
13581399 try transitive_deps.add(self.link_objects.items);
13591400
13601401 var prev_has_cflags = false;
1402 var prev_has_rcflags = false;
13611403 var prev_search_strategy: SystemLib.SearchStrategy = .paths_first;
13621404 var prev_preferred_link_mode: std.builtin.LinkMode = .Dynamic;
13631405
......@@ -1500,6 +1542,24 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
15001542 try zig_args.append(b.pathFromRoot(file));
15011543 }
15021544 },
1545
1546 .win32_resource_file => |rc_source_file| {
1547 if (rc_source_file.flags.len == 0) {
1548 if (prev_has_rcflags) {
1549 try zig_args.append("-rcflags");
1550 try zig_args.append("--");
1551 prev_has_rcflags = false;
1552 }
1553 } else {
1554 try zig_args.append("-rcflags");
1555 for (rc_source_file.flags) |arg| {
1556 try zig_args.append(arg);
1557 }
1558 try zig_args.append("--");
1559 prev_has_rcflags = true;
1560 }
1561 try zig_args.append(rc_source_file.file.getPath(b));
1562 },
15031563 }
15041564 }
15051565
......@@ -1897,6 +1957,11 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
18971957 }
18981958 }
18991959
1960 if (self.rc_includes != .any) {
1961 try zig_args.append("-rcincludes");
1962 try zig_args.append(@tagName(self.rc_includes));
1963 }
1964
19001965 try addFlag(&zig_args, "valgrind", self.valgrind_support);
19011966 try addFlag(&zig_args, "each-lib-rpath", self.each_lib_rpath);
19021967
lib/std/zig/ErrorBundle.zig+20-2
......@@ -421,7 +421,7 @@ pub const Wip = struct {
421421 _ = try addExtra(wip, rt);
422422 }
423423
424 pub fn addBundle(wip: *Wip, other: ErrorBundle) !void {
424 pub fn addBundleAsNotes(wip: *Wip, other: ErrorBundle) !void {
425425 const gpa = wip.gpa;
426426
427427 try wip.string_bytes.ensureUnusedCapacity(gpa, other.string_bytes.len);
......@@ -436,6 +436,21 @@ pub const Wip = struct {
436436 }
437437 }
438438
439 pub fn addBundleAsRoots(wip: *Wip, other: ErrorBundle) !void {
440 const gpa = wip.gpa;
441
442 try wip.string_bytes.ensureUnusedCapacity(gpa, other.string_bytes.len);
443 try wip.extra.ensureUnusedCapacity(gpa, other.extra.len);
444
445 const other_list = other.getMessages();
446
447 try wip.root_list.ensureUnusedCapacity(gpa, other_list.len);
448 for (other_list) |other_msg| {
449 // The ensureUnusedCapacity calls above guarantees this.
450 wip.root_list.appendAssumeCapacity(wip.addOtherMessage(other, other_msg) catch unreachable);
451 }
452 }
453
439454 pub fn reserveNotes(wip: *Wip, notes_len: u32) !u32 {
440455 try wip.extra.ensureUnusedCapacity(wip.gpa, notes_len +
441456 notes_len * @typeInfo(ErrorBundle.ErrorMessage).Struct.fields.len);
......@@ -474,7 +489,10 @@ pub const Wip = struct {
474489 .span_start = other_sl.span_start,
475490 .span_main = other_sl.span_main,
476491 .span_end = other_sl.span_end,
477 .source_line = try wip.addString(other.nullTerminatedString(other_sl.source_line)),
492 .source_line = if (other_sl.source_line != 0)
493 try wip.addString(other.nullTerminatedString(other_sl.source_line))
494 else
495 0,
478496 .reference_trace_len = other_sl.reference_trace_len,
479497 });
480498
src/Compilation.zig+763-8
......@@ -39,6 +39,7 @@ const libtsan = @import("libtsan.zig");
3939const Zir = @import("Zir.zig");
4040const Autodoc = @import("Autodoc.zig");
4141const Color = @import("main.zig").Color;
42const resinator = @import("resinator.zig");
4243
4344/// General-purpose allocator. Used for both temporary and long-term storage.
4445gpa: Allocator,
......@@ -46,6 +47,7 @@ gpa: Allocator,
4647arena_state: std.heap.ArenaAllocator.State,
4748bin_file: *link.File,
4849c_object_table: std.AutoArrayHashMapUnmanaged(*CObject, void) = .{},
50win32_resource_table: std.AutoArrayHashMapUnmanaged(*Win32Resource, void) = .{},
4951/// This is a pointer to a local variable inside `update()`.
5052whole_cache_manifest: ?*Cache.Manifest = null,
5153whole_cache_manifest_mutex: std.Thread.Mutex = .{},
......@@ -60,6 +62,10 @@ anon_work_queue: std.fifo.LinearFifo(Job, .Dynamic),
6062/// gets linked with the Compilation.
6163c_object_work_queue: std.fifo.LinearFifo(*CObject, .Dynamic),
6264
65/// These jobs are to invoke the RC compiler to create a compiled resource file (.res), which
66/// gets linked with the Compilation.
67win32_resource_work_queue: std.fifo.LinearFifo(*Win32Resource, .Dynamic),
68
6369/// These jobs are to tokenize, parse, and astgen files, which may be outdated
6470/// since the last compilation, as well as scan for `@import` and queue up
6571/// additional jobs corresponding to those new files.
......@@ -73,6 +79,10 @@ embed_file_work_queue: std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic),
7379/// This data is accessed by multiple threads and is protected by `mutex`.
7480failed_c_objects: std.AutoArrayHashMapUnmanaged(*CObject, *CObject.ErrorMsg) = .{},
7581
82/// The ErrorBundle memory is owned by the `Win32Resource`, using Compilation's general purpose allocator.
83/// This data is accessed by multiple threads and is protected by `mutex`.
84failed_win32_resources: std.AutoArrayHashMapUnmanaged(*Win32Resource, ErrorBundle) = .{},
85
7686/// Miscellaneous things that can fail.
7787misc_failures: std.AutoArrayHashMapUnmanaged(MiscTask, MiscError) = .{},
7888
......@@ -109,6 +119,7 @@ last_update_was_cache_hit: bool = false,
109119
110120c_source_files: []const CSourceFile,
111121clang_argv: []const []const u8,
122rc_source_files: []const RcSourceFile,
112123cache_parent: *Cache,
113124/// Path to own executable for invoking `zig clang`.
114125self_exe_path: ?[]const u8,
......@@ -125,6 +136,7 @@ local_cache_directory: Directory,
125136global_cache_directory: Directory,
126137libc_include_dir_list: []const []const u8,
127138libc_framework_dir_list: []const []const u8,
139rc_include_dir_list: []const []const u8,
128140thread_pool: *ThreadPool,
129141
130142/// Populated when we build the libc++ static library. A Job to build this is placed in the queue
......@@ -225,6 +237,23 @@ pub const CSourceFile = struct {
225237 ext: ?FileExt = null,
226238};
227239
240/// For passing to resinator.
241pub const RcSourceFile = struct {
242 src_path: []const u8,
243 extra_flags: []const []const u8 = &.{},
244};
245
246pub const RcIncludes = enum {
247 /// Use MSVC if available, fall back to MinGW.
248 any,
249 /// Use MSVC include paths (MSVC install + Windows SDK, must be present on the system).
250 msvc,
251 /// Use MinGW include paths (distributed with Zig).
252 gnu,
253 /// Do not use any autodetected include paths.
254 none,
255};
256
228257const Job = union(enum) {
229258 /// Write the constant value for a Decl to the output file.
230259 codegen_decl: Module.Decl.Index,
......@@ -326,6 +355,50 @@ pub const CObject = struct {
326355 }
327356};
328357
358pub const Win32Resource = struct {
359 /// Relative to cwd. Owned by arena.
360 src: RcSourceFile,
361 status: union(enum) {
362 new,
363 success: struct {
364 /// The outputted result. Owned by gpa.
365 res_path: []u8,
366 /// This is a file system lock on the cache hash manifest representing this
367 /// object. It prevents other invocations of the Zig compiler from interfering
368 /// with this object until released.
369 lock: Cache.Lock,
370 },
371 /// There will be a corresponding ErrorMsg in Compilation.failed_win32_resources.
372 failure,
373 /// A transient failure happened when trying to compile the resource file; it may
374 /// succeed if we try again. There may be a corresponding ErrorMsg in
375 /// Compilation.failed_win32_resources. If there is not, the failure is out of memory.
376 failure_retryable,
377 },
378
379 /// Returns true if there was failure.
380 pub fn clearStatus(self: *Win32Resource, gpa: Allocator) bool {
381 switch (self.status) {
382 .new => return false,
383 .failure, .failure_retryable => {
384 self.status = .new;
385 return true;
386 },
387 .success => |*success| {
388 gpa.free(success.res_path);
389 success.lock.release();
390 self.status = .new;
391 return false;
392 },
393 }
394 }
395
396 pub fn destroy(self: *Win32Resource, gpa: Allocator) void {
397 _ = self.clearStatus(gpa);
398 gpa.destroy(self);
399 }
400};
401
329402pub const MiscTask = enum {
330403 write_builtin_zig,
331404 glibc_crt_file,
......@@ -505,6 +578,8 @@ pub const InitOptions = struct {
505578 rpath_list: []const []const u8 = &[0][]const u8{},
506579 symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .{},
507580 c_source_files: []const CSourceFile = &[0]CSourceFile{},
581 rc_source_files: []const RcSourceFile = &[0]RcSourceFile{},
582 rc_includes: RcIncludes = .any,
508583 link_objects: []LinkObject = &[0]LinkObject{},
509584 framework_dirs: []const []const u8 = &[0][]const u8{},
510585 frameworks: []const Framework = &.{},
......@@ -938,6 +1013,11 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
9381013 options.libc_installation,
9391014 );
9401015
1016 const rc_dirs = try detectWin32ResourceIncludeDirs(
1017 arena,
1018 options,
1019 );
1020
9411021 const sysroot = options.sysroot orelse libc_dirs.sysroot;
9421022
9431023 const must_pie = target_util.requiresPIE(options.target);
......@@ -1591,16 +1671,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
15911671 .work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa),
15921672 .anon_work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa),
15931673 .c_object_work_queue = std.fifo.LinearFifo(*CObject, .Dynamic).init(gpa),
1674 .win32_resource_work_queue = std.fifo.LinearFifo(*Win32Resource, .Dynamic).init(gpa),
15941675 .astgen_work_queue = std.fifo.LinearFifo(*Module.File, .Dynamic).init(gpa),
15951676 .embed_file_work_queue = std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic).init(gpa),
15961677 .keep_source_files_loaded = options.keep_source_files_loaded,
15971678 .use_clang = use_clang,
15981679 .clang_argv = options.clang_argv,
15991680 .c_source_files = options.c_source_files,
1681 .rc_source_files = options.rc_source_files,
16001682 .cache_parent = cache,
16011683 .self_exe_path = options.self_exe_path,
16021684 .libc_include_dir_list = libc_dirs.libc_include_dir_list,
16031685 .libc_framework_dir_list = libc_dirs.libc_framework_dir_list,
1686 .rc_include_dir_list = rc_dirs.libc_include_dir_list,
16041687 .sanitize_c = sanitize_c,
16051688 .thread_pool = options.thread_pool,
16061689 .clang_passthrough_mode = options.clang_passthrough_mode,
......@@ -1647,6 +1730,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
16471730 comp.c_object_table.putAssumeCapacityNoClobber(c_object, {});
16481731 }
16491732
1733 // Add a `Win32Resource` for each `rc_source_files`.
1734 try comp.win32_resource_table.ensureTotalCapacity(gpa, options.rc_source_files.len);
1735 for (options.rc_source_files) |rc_source_file| {
1736 const win32_resource = try gpa.create(Win32Resource);
1737 errdefer gpa.destroy(win32_resource);
1738
1739 win32_resource.* = .{
1740 .status = .{ .new = {} },
1741 .src = rc_source_file,
1742 };
1743 comp.win32_resource_table.putAssumeCapacityNoClobber(win32_resource, {});
1744 }
1745
16501746 const have_bin_emit = comp.bin_file.options.emit != null or comp.whole_bin_sub_path != null;
16511747
16521748 if (have_bin_emit and !comp.bin_file.options.skip_linker_dependencies and target.ofmt != .c) {
......@@ -1804,6 +1900,7 @@ pub fn destroy(self: *Compilation) void {
18041900 self.work_queue.deinit();
18051901 self.anon_work_queue.deinit();
18061902 self.c_object_work_queue.deinit();
1903 self.win32_resource_work_queue.deinit();
18071904 self.astgen_work_queue.deinit();
18081905 self.embed_file_work_queue.deinit();
18091906
......@@ -1852,6 +1949,16 @@ pub fn destroy(self: *Compilation) void {
18521949 }
18531950 self.failed_c_objects.deinit(gpa);
18541951
1952 for (self.win32_resource_table.keys()) |key| {
1953 key.destroy(gpa);
1954 }
1955 self.win32_resource_table.deinit(gpa);
1956
1957 for (self.failed_win32_resources.values()) |*value| {
1958 value.deinit(gpa);
1959 }
1960 self.failed_win32_resources.deinit(gpa);
1961
18551962 for (self.lld_errors.items) |*lld_error| {
18561963 lld_error.deinit(gpa);
18571964 }
......@@ -2014,6 +2121,13 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void
20142121 comp.c_object_work_queue.writeItemAssumeCapacity(key);
20152122 }
20162123
2124 // For compiling Win32 resources, we rely on the cache hash system to avoid duplicating work.
2125 // Add a Job for each Win32 resource file.
2126 try comp.win32_resource_work_queue.ensureUnusedCapacity(comp.win32_resource_table.count());
2127 for (comp.win32_resource_table.keys()) |key| {
2128 comp.win32_resource_work_queue.writeItemAssumeCapacity(key);
2129 }
2130
20172131 if (comp.bin_file.options.module) |module| {
20182132 module.compile_log_text.shrinkAndFree(module.gpa, 0);
20192133 module.generation += 1;
......@@ -2336,6 +2450,13 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes
23362450 man.hash.addListOfBytes(key.src.extra_flags);
23372451 }
23382452
2453 for (comp.win32_resource_table.keys()) |key| {
2454 _ = try man.addFile(key.src.src_path, null);
2455 man.hash.addListOfBytes(key.src.extra_flags);
2456 }
2457
2458 man.hash.addListOfBytes(comp.rc_include_dir_list);
2459
23392460 cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_asm);
23402461 cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_ir);
23412462 cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_bc);
......@@ -2571,8 +2692,14 @@ pub fn makeBinFileWritable(self: *Compilation) !void {
25712692
25722693/// This function is temporally single-threaded.
25732694pub fn totalErrorCount(self: *Compilation) u32 {
2574 var total: usize = self.failed_c_objects.count() + self.misc_failures.count() +
2575 @intFromBool(self.alloc_failure_occurred) + self.lld_errors.items.len;
2695 var total: usize = self.failed_c_objects.count() +
2696 self.misc_failures.count() +
2697 @intFromBool(self.alloc_failure_occurred) +
2698 self.lld_errors.items.len;
2699
2700 for (self.failed_win32_resources.values()) |errs| {
2701 total += errs.errorMessageCount();
2702 }
25762703
25772704 if (self.bin_file.options.module) |module| {
25782705 total += module.failed_exports.count();
......@@ -2664,6 +2791,13 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle {
26642791 }
26652792 }
26662793
2794 {
2795 var it = self.failed_win32_resources.iterator();
2796 while (it.next()) |entry| {
2797 try bundle.addBundleAsRoots(entry.value_ptr.*);
2798 }
2799 }
2800
26672801 for (self.lld_errors.items) |lld_error| {
26682802 const notes_len = @as(u32, @intCast(lld_error.context_lines.len));
26692803
......@@ -2683,7 +2817,7 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle {
26832817 .msg = try bundle.addString(value.msg),
26842818 .notes_len = if (value.children) |b| b.errorMessageCount() else 0,
26852819 });
2686 if (value.children) |b| try bundle.addBundle(b);
2820 if (value.children) |b| try bundle.addBundleAsNotes(b);
26872821 }
26882822 if (self.alloc_failure_occurred) {
26892823 try bundle.addRootErrorMessage(.{
......@@ -3082,6 +3216,9 @@ pub fn performAllTheWork(
30823216 var c_obj_prog_node = main_progress_node.start("Compile C Objects", comp.c_source_files.len);
30833217 defer c_obj_prog_node.end();
30843218
3219 var win32_resource_prog_node = main_progress_node.start("Compile Win32 Resources", comp.rc_source_files.len);
3220 defer win32_resource_prog_node.end();
3221
30853222 var embed_file_prog_node = main_progress_node.start("Detect @embedFile updates", comp.embed_file_work_queue.count);
30863223 defer embed_file_prog_node.end();
30873224
......@@ -3130,6 +3267,13 @@ pub fn performAllTheWork(
31303267 comp, c_object, &c_obj_prog_node, &comp.work_queue_wait_group,
31313268 });
31323269 }
3270
3271 while (comp.win32_resource_work_queue.readItem()) |win32_resource| {
3272 comp.work_queue_wait_group.start();
3273 try comp.thread_pool.spawn(workerUpdateWin32Resource, .{
3274 comp, win32_resource, &win32_resource_prog_node, &comp.work_queue_wait_group,
3275 });
3276 }
31333277 }
31343278
31353279 if (comp.bin_file.options.module) |mod| {
......@@ -3659,6 +3803,14 @@ pub fn obtainCObjectCacheManifest(comp: *const Compilation) Cache.Manifest {
36593803 return man;
36603804}
36613805
3806pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest {
3807 var man = comp.cache_parent.obtain();
3808
3809 man.hash.addListOfBytes(comp.rc_include_dir_list);
3810
3811 return man;
3812}
3813
36623814test "cImport" {
36633815 _ = cImport;
36643816}
......@@ -3832,6 +3984,26 @@ fn workerUpdateCObject(
38323984 };
38333985}
38343986
3987fn workerUpdateWin32Resource(
3988 comp: *Compilation,
3989 win32_resource: *Win32Resource,
3990 progress_node: *std.Progress.Node,
3991 wg: *WaitGroup,
3992) void {
3993 defer wg.finish();
3994
3995 comp.updateWin32Resource(win32_resource, progress_node) catch |err| switch (err) {
3996 error.AnalysisFail => return,
3997 else => {
3998 comp.reportRetryableWin32ResourceError(win32_resource, err) catch |oom| switch (oom) {
3999 // Swallowing this error is OK because it's implied to be OOM when
4000 // there is a missing failed_win32_resources error message.
4001 error.OutOfMemory => {},
4002 };
4003 },
4004 };
4005}
4006
38354007fn buildCompilerRtOneShot(
38364008 comp: *Compilation,
38374009 output_mode: std.builtin.OutputMode,
......@@ -3877,6 +4049,18 @@ fn reportRetryableCObjectError(
38774049 }
38784050}
38794051
4052fn reportRetryableWin32ResourceError(
4053 comp: *Compilation,
4054 win32_resource: *Win32Resource,
4055 err: anyerror,
4056) error{OutOfMemory}!void {
4057 win32_resource.status = .failure_retryable;
4058
4059 // TODO: something
4060 _ = comp;
4061 _ = @errorName(err);
4062}
4063
38804064fn reportRetryableAstGenError(
38814065 comp: *Compilation,
38824066 src: AstGenSrc,
......@@ -4233,6 +4417,311 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
42334417 };
42344418}
42354419
4420fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void {
4421 if (!build_options.have_llvm) {
4422 return comp.failWin32Resource(win32_resource, "clang not available: compiler built without LLVM extensions", .{});
4423 }
4424 const self_exe_path = comp.self_exe_path orelse
4425 return comp.failWin32Resource(win32_resource, "clang compilation disabled", .{});
4426
4427 const tracy_trace = trace(@src());
4428 defer tracy_trace.end();
4429
4430 log.debug("updating win32 resource: {s}", .{win32_resource.src.src_path});
4431
4432 if (win32_resource.clearStatus(comp.gpa)) {
4433 // There was previous failure.
4434 comp.mutex.lock();
4435 defer comp.mutex.unlock();
4436 // If the failure was OOM, there will not be an entry here, so we do
4437 // not assert discard.
4438 _ = comp.failed_win32_resources.swapRemove(win32_resource);
4439 }
4440
4441 var man = comp.obtainWin32ResourceCacheManifest();
4442 defer man.deinit();
4443
4444 _ = try man.addFile(win32_resource.src.src_path, null);
4445 man.hash.addListOfBytes(win32_resource.src.extra_flags);
4446
4447 var arena_allocator = std.heap.ArenaAllocator.init(comp.gpa);
4448 defer arena_allocator.deinit();
4449 const arena = arena_allocator.allocator();
4450
4451 const rc_basename = std.fs.path.basename(win32_resource.src.src_path);
4452
4453 win32_resource_prog_node.activate();
4454 var child_progress_node = win32_resource_prog_node.start(rc_basename, 0);
4455 child_progress_node.activate();
4456 defer child_progress_node.end();
4457
4458 const rc_basename_noext = rc_basename[0 .. rc_basename.len - std.fs.path.extension(rc_basename).len];
4459
4460 const digest = if (try man.hit()) man.final() else blk: {
4461 const rcpp_filename = try std.fmt.allocPrint(arena, "{s}.rcpp", .{rc_basename_noext});
4462
4463 const out_rcpp_path = try comp.tmpFilePath(arena, rcpp_filename);
4464 var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{});
4465 defer zig_cache_tmp_dir.close();
4466
4467 const res_filename = try std.fmt.allocPrint(arena, "{s}.res", .{rc_basename_noext});
4468
4469 // We can't know the digest until we do the compilation,
4470 // so we need a temporary filename.
4471 const out_res_path = try comp.tmpFilePath(arena, res_filename);
4472
4473 var options = options: {
4474 var resinator_args = try std.ArrayListUnmanaged([]const u8).initCapacity(comp.gpa, win32_resource.src.extra_flags.len + 4);
4475 defer resinator_args.deinit(comp.gpa);
4476
4477 resinator_args.appendAssumeCapacity(""); // dummy 'process name' arg
4478 resinator_args.appendSliceAssumeCapacity(win32_resource.src.extra_flags);
4479 resinator_args.appendSliceAssumeCapacity(&.{ "--", out_rcpp_path, out_res_path });
4480
4481 var cli_diagnostics = resinator.cli.Diagnostics.init(comp.gpa);
4482 defer cli_diagnostics.deinit();
4483 var options = resinator.cli.parse(comp.gpa, resinator_args.items, &cli_diagnostics) catch |err| switch (err) {
4484 error.ParseError => {
4485 return comp.failWin32ResourceCli(win32_resource, &cli_diagnostics);
4486 },
4487 else => |e| return e,
4488 };
4489 break :options options;
4490 };
4491 defer options.deinit();
4492
4493 var argv = std.ArrayList([]const u8).init(comp.gpa);
4494 defer argv.deinit();
4495 var temp_strings = std.ArrayList([]const u8).init(comp.gpa);
4496 defer {
4497 for (temp_strings.items) |temp_string| {
4498 comp.gpa.free(temp_string);
4499 }
4500 temp_strings.deinit();
4501 }
4502
4503 // TODO: support options.preprocess == .no and .only
4504 // alternatively, error if those options are used
4505 try argv.appendSlice(&[_][]const u8{
4506 self_exe_path,
4507 "clang",
4508 "-E", // preprocessor only
4509 "--comments",
4510 "-fuse-line-directives", // #line <num> instead of # <num>
4511 "-xc", // output c
4512 "-Werror=null-character", // error on null characters instead of converting them to spaces
4513 "-fms-compatibility", // Allow things like "header.h" to be resolved relative to the 'root' .rc file, among other things
4514 "-DRC_INVOKED", // https://learn.microsoft.com/en-us/windows/win32/menurc/predefined-macros
4515 });
4516 // Using -fms-compatibility and targeting the gnu abi interact in a strange way:
4517 // - Targeting the GNU abi stops _MSC_VER from being defined
4518 // - Passing -fms-compatibility stops __GNUC__ from being defined
4519 // Neither being defined is a problem for things like things like MinGW's
4520 // vadefs.h, which will fail during preprocessing if neither are defined.
4521 // So, when targeting the GNU abi, we need to force __GNUC__ to be defined.
4522 //
4523 // TODO: This is a workaround that should be removed if possible.
4524 if (comp.getTarget().isGnu()) {
4525 // This is the same default gnuc version that Clang uses:
4526 // https://github.com/llvm/llvm-project/blob/4b5366c9512aa273a5272af1d833961e1ed156e7/clang/lib/Driver/ToolChains/Clang.cpp#L6738
4527 try argv.append("-fgnuc-version=4.2.1");
4528 }
4529 for (options.extra_include_paths.items) |extra_include_path| {
4530 try argv.append("--include-directory");
4531 try argv.append(extra_include_path);
4532 }
4533 var symbol_it = options.symbols.iterator();
4534 while (symbol_it.next()) |entry| {
4535 switch (entry.value_ptr.*) {
4536 .define => |value| {
4537 try argv.append("-D");
4538 const define_arg = arg: {
4539 const arg = try std.fmt.allocPrint(comp.gpa, "{s}={s}", .{ entry.key_ptr.*, value });
4540 errdefer comp.gpa.free(arg);
4541 try temp_strings.append(arg);
4542 break :arg arg;
4543 };
4544 try argv.append(define_arg);
4545 },
4546 .undefine => {
4547 try argv.append("-U");
4548 try argv.append(entry.key_ptr.*);
4549 },
4550 }
4551 }
4552 try argv.append(win32_resource.src.src_path);
4553 try argv.appendSlice(&[_][]const u8{
4554 "-o",
4555 out_rcpp_path,
4556 });
4557
4558 const out_dep_path = try std.fmt.allocPrint(arena, "{s}.d", .{out_rcpp_path});
4559 // Note: addCCArgs will implicitly add _DEBUG/NDEBUG depending on the optimization
4560 // mode. While these defines are not normally present when calling rc.exe directly,
4561 // them being defined matches the behavior of how MSVC calls rc.exe which is the more
4562 // relevant behavior in this case.
4563 try comp.addCCArgs(arena, &argv, .rc, out_dep_path);
4564
4565 if (comp.verbose_cc) {
4566 dump_argv(argv.items);
4567 }
4568
4569 if (std.process.can_spawn) {
4570 var child = std.ChildProcess.init(argv.items, arena);
4571 child.stdin_behavior = .Ignore;
4572 child.stdout_behavior = .Ignore;
4573 child.stderr_behavior = .Pipe;
4574
4575 try child.spawn();
4576
4577 const stderr_reader = child.stderr.?.reader();
4578
4579 const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
4580
4581 const term = child.wait() catch |err| {
4582 return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
4583 };
4584
4585 switch (term) {
4586 .Exited => |code| {
4587 if (code != 0) {
4588 // TODO parse clang stderr and turn it into an error message
4589 // and then call failCObjWithOwnedErrorMsg
4590 log.err("clang preprocessor failed with stderr:\n{s}", .{stderr});
4591 return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{code});
4592 }
4593 },
4594 else => {
4595 log.err("clang preprocessor terminated with stderr:\n{s}", .{stderr});
4596 return comp.failWin32Resource(win32_resource, "clang preprocessor terminated unexpectedly", .{});
4597 },
4598 }
4599 } else {
4600 const exit_code = try clangMain(arena, argv.items);
4601 if (exit_code != 0) {
4602 return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{exit_code});
4603 }
4604 }
4605
4606 const dep_basename = std.fs.path.basename(out_dep_path);
4607 // Add the files depended on to the cache system.
4608 try man.addDepFilePost(zig_cache_tmp_dir, dep_basename);
4609 if (comp.whole_cache_manifest) |whole_cache_manifest| {
4610 comp.whole_cache_manifest_mutex.lock();
4611 defer comp.whole_cache_manifest_mutex.unlock();
4612 try whole_cache_manifest.addDepFilePost(zig_cache_tmp_dir, dep_basename);
4613 }
4614 // Just to save disk space, we delete the file because it is never needed again.
4615 zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| {
4616 log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) });
4617 };
4618
4619 var full_input = std.fs.cwd().readFileAlloc(arena, out_rcpp_path, std.math.maxInt(usize)) catch |err| switch (err) {
4620 error.OutOfMemory => return error.OutOfMemory,
4621 else => |e| {
4622 return comp.failWin32Resource(win32_resource, "failed to read preprocessed file '{s}': {s}", .{ out_rcpp_path, @errorName(e) });
4623 },
4624 };
4625
4626 var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(arena, full_input, full_input, .{ .initial_filename = win32_resource.src.src_path });
4627 defer mapping_results.mappings.deinit(arena);
4628
4629 var final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
4630
4631 var output_file = zig_cache_tmp_dir.createFile(out_res_path, .{}) catch |err| {
4632 return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ out_res_path, @errorName(err) });
4633 };
4634 var output_file_closed = false;
4635 defer if (!output_file_closed) output_file.close();
4636
4637 var diagnostics = resinator.errors.Diagnostics.init(arena);
4638 defer diagnostics.deinit();
4639
4640 var dependencies_list = std.ArrayList([]const u8).init(comp.gpa);
4641 defer {
4642 for (dependencies_list.items) |item| {
4643 comp.gpa.free(item);
4644 }
4645 dependencies_list.deinit();
4646 }
4647
4648 var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
4649
4650 resinator.compile.compile(arena, final_input, output_buffered_stream.writer(), .{
4651 .cwd = std.fs.cwd(),
4652 .diagnostics = &diagnostics,
4653 .source_mappings = &mapping_results.mappings,
4654 .dependencies_list = &dependencies_list,
4655 .system_include_paths = comp.rc_include_dir_list,
4656 .ignore_include_env_var = true,
4657 // options
4658 .extra_include_paths = options.extra_include_paths.items,
4659 .default_language_id = options.default_language_id,
4660 .default_code_page = options.default_code_page orelse .windows1252,
4661 .verbose = options.verbose,
4662 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
4663 .max_string_literal_codepoints = options.max_string_literal_codepoints,
4664 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
4665 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
4666 }) catch |err| switch (err) {
4667 error.ParseError, error.CompileError => {
4668 // Delete the output file on error
4669 output_file.close();
4670 output_file_closed = true;
4671 // Failing to delete is not really a big deal, so swallow any errors
4672 zig_cache_tmp_dir.deleteFile(out_res_path) catch {
4673 log.warn("failed to delete '{s}': {s}", .{ out_res_path, @errorName(err) });
4674 };
4675 return comp.failWin32ResourceCompile(win32_resource, final_input, &diagnostics, mapping_results.mappings);
4676 },
4677 else => |e| return e,
4678 };
4679
4680 try output_buffered_stream.flush();
4681
4682 for (dependencies_list.items) |dep_file_path| {
4683 try man.addFilePost(dep_file_path);
4684 if (comp.whole_cache_manifest) |whole_cache_manifest| {
4685 comp.whole_cache_manifest_mutex.lock();
4686 defer comp.whole_cache_manifest_mutex.unlock();
4687 try whole_cache_manifest.addFilePost(dep_file_path);
4688 }
4689 }
4690
4691 // Rename into place.
4692 const digest = man.final();
4693 const o_sub_path = try std.fs.path.join(arena, &[_][]const u8{ "o", &digest });
4694 var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{});
4695 defer o_dir.close();
4696 const tmp_basename = std.fs.path.basename(out_res_path);
4697 try std.fs.rename(zig_cache_tmp_dir, tmp_basename, o_dir, res_filename);
4698 const tmp_rcpp_basename = std.fs.path.basename(out_rcpp_path);
4699 try std.fs.rename(zig_cache_tmp_dir, tmp_rcpp_basename, o_dir, rcpp_filename);
4700 break :blk digest;
4701 };
4702
4703 if (man.have_exclusive_lock) {
4704 // Write the updated manifest. This is a no-op if the manifest is not dirty. Note that it is
4705 // possible we had a hit and the manifest is dirty, for example if the file mtime changed but
4706 // the contents were the same, we hit the cache but the manifest is dirty and we need to update
4707 // it to prevent doing a full file content comparison the next time around.
4708 man.writeManifest() catch |err| {
4709 log.warn("failed to write cache manifest when compiling '{s}': {s}", .{ win32_resource.src.src_path, @errorName(err) });
4710 };
4711 }
4712
4713 const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{rc_basename_noext});
4714
4715 win32_resource.status = .{
4716 .success = .{
4717 .res_path = try comp.local_cache_directory.join(comp.gpa, &[_][]const u8{
4718 "o", &digest, res_basename,
4719 }),
4720 .lock = man.toOwnedLock(),
4721 },
4722 };
4723}
4724
42364725pub fn tmpFilePath(comp: *Compilation, ally: Allocator, suffix: []const u8) error{OutOfMemory}![]const u8 {
42374726 const s = std.fs.path.sep_str;
42384727 const rand_int = std.crypto.random.int(u64);
......@@ -4350,7 +4839,7 @@ pub fn addCCArgs(
43504839 try argv.appendSlice(&[_][]const u8{ "-target", llvm_triple });
43514840
43524841 switch (ext) {
4353 .c, .cpp, .m, .mm, .h, .cu => {
4842 .c, .cpp, .m, .mm, .h, .cu, .rc => {
43544843 try argv.appendSlice(&[_][]const u8{
43554844 "-nostdinc",
43564845 "-fno-spell-checking",
......@@ -4378,9 +4867,16 @@ pub fn addCCArgs(
43784867 try argv.append("-isystem");
43794868 try argv.append(c_headers_dir);
43804869
4381 for (comp.libc_include_dir_list) |include_dir| {
4382 try argv.append("-isystem");
4383 try argv.append(include_dir);
4870 if (ext == .rc) {
4871 for (comp.rc_include_dir_list) |include_dir| {
4872 try argv.append("-isystem");
4873 try argv.append(include_dir);
4874 }
4875 } else {
4876 for (comp.libc_include_dir_list) |include_dir| {
4877 try argv.append("-isystem");
4878 try argv.append(include_dir);
4879 }
43844880 }
43854881
43864882 if (target.cpu.model.llvm_name) |llvm_name| {
......@@ -4692,6 +5188,253 @@ fn failCObjWithOwnedErrorMsg(
46925188 return error.AnalysisFail;
46935189}
46945190
5191/// The include directories used when preprocessing .rc files are separate from the
5192/// target. Which include directories are used is determined by `options.rc_includes`.
5193///
5194/// Note: It should be okay that the include directories used when compiling .rc
5195/// files differ from the include directories used when compiling the main
5196/// binary, since the .res format is not dependent on anything ABI-related. The
5197/// only relevant differences would be things like `#define` constants being
5198/// different in the MinGW headers vs the MSVC headers, but any such
5199/// differences would likely be a MinGW bug.
5200fn detectWin32ResourceIncludeDirs(arena: Allocator, options: InitOptions) !LibCDirs {
5201 // Set the includes to .none here when there are no rc files to compile
5202 var includes = if (options.rc_source_files.len > 0) options.rc_includes else .none;
5203 if (builtin.target.os.tag != .windows) {
5204 switch (includes) {
5205 // MSVC can't be found when the host isn't Windows, so short-circuit.
5206 .msvc => return error.WindowsSdkNotFound,
5207 // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts.
5208 .any => includes = .gnu,
5209 .none, .gnu => {},
5210 }
5211 }
5212 while (true) {
5213 switch (includes) {
5214 .any, .msvc => return detectLibCIncludeDirs(
5215 arena,
5216 options.zig_lib_directory.path.?,
5217 .{
5218 .cpu = options.target.cpu,
5219 .os = options.target.os,
5220 .abi = .msvc,
5221 .ofmt = options.target.ofmt,
5222 },
5223 options.is_native_abi,
5224 // The .rc preprocessor will need to know the libc include dirs even if we
5225 // are not linking libc, so force 'link_libc' to true
5226 true,
5227 options.libc_installation,
5228 ) catch |err| {
5229 if (includes == .any) {
5230 // fall back to mingw
5231 includes = .gnu;
5232 continue;
5233 }
5234 return err;
5235 },
5236 .gnu => return detectLibCFromBuilding(arena, options.zig_lib_directory.path.?, .{
5237 .cpu = options.target.cpu,
5238 .os = options.target.os,
5239 .abi = .gnu,
5240 .ofmt = options.target.ofmt,
5241 }),
5242 .none => return LibCDirs{
5243 .libc_include_dir_list = &[0][]u8{},
5244 .libc_installation = null,
5245 .libc_framework_dir_list = &.{},
5246 .sysroot = null,
5247 },
5248 }
5249 }
5250}
5251
5252fn failWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, comptime format: []const u8, args: anytype) SemaError {
5253 @setCold(true);
5254 var bundle: ErrorBundle.Wip = undefined;
5255 try bundle.init(comp.gpa);
5256 errdefer bundle.deinit();
5257 try bundle.addRootErrorMessage(.{
5258 .msg = try bundle.printString(format, args),
5259 .src_loc = try bundle.addSourceLocation(.{
5260 .src_path = try bundle.addString(win32_resource.src.src_path),
5261 .line = 0,
5262 .column = 0,
5263 .span_start = 0,
5264 .span_main = 0,
5265 .span_end = 0,
5266 }),
5267 });
5268 const finished_bundle = try bundle.toOwnedBundle("");
5269 return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle);
5270}
5271
5272fn failWin32ResourceWithOwnedBundle(
5273 comp: *Compilation,
5274 win32_resource: *Win32Resource,
5275 err_bundle: ErrorBundle,
5276) SemaError {
5277 @setCold(true);
5278 {
5279 comp.mutex.lock();
5280 defer comp.mutex.unlock();
5281 try comp.failed_win32_resources.putNoClobber(comp.gpa, win32_resource, err_bundle);
5282 }
5283 win32_resource.status = .failure;
5284 return error.AnalysisFail;
5285}
5286
5287fn failWin32ResourceCli(
5288 comp: *Compilation,
5289 win32_resource: *Win32Resource,
5290 diagnostics: *resinator.cli.Diagnostics,
5291) SemaError {
5292 @setCold(true);
5293
5294 var bundle: ErrorBundle.Wip = undefined;
5295 try bundle.init(comp.gpa);
5296 errdefer bundle.deinit();
5297
5298 try bundle.addRootErrorMessage(.{
5299 .msg = try bundle.addString("invalid command line option(s)"),
5300 .src_loc = try bundle.addSourceLocation(.{
5301 .src_path = try bundle.addString(win32_resource.src.src_path),
5302 .line = 0,
5303 .column = 0,
5304 .span_start = 0,
5305 .span_main = 0,
5306 .span_end = 0,
5307 }),
5308 });
5309
5310 var cur_err: ?ErrorBundle.ErrorMessage = null;
5311 var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
5312 defer cur_notes.deinit(comp.gpa);
5313 for (diagnostics.errors.items) |err_details| {
5314 switch (err_details.type) {
5315 .err => {
5316 if (cur_err) |err| {
5317 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5318 }
5319 cur_err = .{
5320 .msg = try bundle.addString(err_details.msg.items),
5321 };
5322 cur_notes.clearRetainingCapacity();
5323 },
5324 .warning => cur_err = null,
5325 .note => {
5326 if (cur_err == null) continue;
5327 cur_err.?.notes_len += 1;
5328 try cur_notes.append(comp.gpa, .{
5329 .msg = try bundle.addString(err_details.msg.items),
5330 });
5331 },
5332 }
5333 }
5334 if (cur_err) |err| {
5335 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5336 }
5337
5338 const finished_bundle = try bundle.toOwnedBundle("");
5339 return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle);
5340}
5341
5342fn failWin32ResourceCompile(
5343 comp: *Compilation,
5344 win32_resource: *Win32Resource,
5345 source: []const u8,
5346 diagnostics: *resinator.errors.Diagnostics,
5347 mappings: resinator.source_mapping.SourceMappings,
5348) SemaError {
5349 @setCold(true);
5350
5351 var bundle: ErrorBundle.Wip = undefined;
5352 try bundle.init(comp.gpa);
5353 errdefer bundle.deinit();
5354
5355 var msg_buf: std.ArrayListUnmanaged(u8) = .{};
5356 defer msg_buf.deinit(comp.gpa);
5357 var cur_err: ?ErrorBundle.ErrorMessage = null;
5358 var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
5359 defer cur_notes.deinit(comp.gpa);
5360 for (diagnostics.errors.items) |err_details| {
5361 switch (err_details.type) {
5362 .hint => continue,
5363 // Clear the current error so that notes don't bleed into unassociated errors
5364 .warning => {
5365 cur_err = null;
5366 continue;
5367 },
5368 .note => if (cur_err == null) continue,
5369 .err => {},
5370 }
5371 const corresponding_span = mappings.get(err_details.token.line_number);
5372 const corresponding_file = mappings.files.get(corresponding_span.filename_offset);
5373
5374 const source_line_start = err_details.token.getLineStart(source);
5375 const column = err_details.token.calculateColumn(source, 1, source_line_start);
5376 const err_line = corresponding_span.start_line;
5377
5378 msg_buf.clearRetainingCapacity();
5379 try err_details.render(msg_buf.writer(comp.gpa), source, diagnostics.strings.items);
5380
5381 const src_loc = src_loc: {
5382 var src_loc: ErrorBundle.SourceLocation = .{
5383 .src_path = try bundle.addString(corresponding_file),
5384 .line = @intCast(err_line - 1), // 1-based -> 0-based
5385 .column = @intCast(column),
5386 .span_start = 0,
5387 .span_main = 0,
5388 .span_end = 0,
5389 };
5390 if (err_details.print_source_line) {
5391 const source_line = err_details.token.getLine(source, source_line_start);
5392 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
5393 src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len);
5394 src_loc.span_main = @intCast(visual_info.point_offset);
5395 src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len);
5396 src_loc.source_line = try bundle.addString(source_line);
5397 }
5398 break :src_loc try bundle.addSourceLocation(src_loc);
5399 };
5400
5401 switch (err_details.type) {
5402 .err => {
5403 if (cur_err) |err| {
5404 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5405 }
5406 cur_err = .{
5407 .msg = try bundle.addString(msg_buf.items),
5408 .src_loc = src_loc,
5409 };
5410 cur_notes.clearRetainingCapacity();
5411 },
5412 .note => {
5413 cur_err.?.notes_len += 1;
5414 try cur_notes.append(comp.gpa, .{
5415 .msg = try bundle.addString(msg_buf.items),
5416 .src_loc = src_loc,
5417 });
5418 },
5419 .warning, .hint => unreachable,
5420 }
5421 }
5422 if (cur_err) |err| {
5423 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5424 }
5425
5426 const finished_bundle = try bundle.toOwnedBundle("");
5427 return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle);
5428}
5429
5430fn win32ResourceFlushErrorMessage(wip: *ErrorBundle.Wip, msg: ErrorBundle.ErrorMessage, notes: []const ErrorBundle.ErrorMessage) !void {
5431 try wip.addRootErrorMessage(msg);
5432 const notes_start = try wip.reserveNotes(@intCast(notes.len));
5433 for (notes_start.., notes) |i, note| {
5434 wip.extra.items[i] = @intFromEnum(wip.addErrorMessageAssumeCapacity(note));
5435 }
5436}
5437
46955438pub const FileExt = enum {
46965439 c,
46975440 cpp,
......@@ -4708,6 +5451,7 @@ pub const FileExt = enum {
47085451 static_library,
47095452 zig,
47105453 def,
5454 rc,
47115455 res,
47125456 unknown,
47135457
......@@ -4724,6 +5468,7 @@ pub const FileExt = enum {
47245468 .static_library,
47255469 .zig,
47265470 .def,
5471 .rc,
47275472 .res,
47285473 .unknown,
47295474 => false,
......@@ -4747,6 +5492,7 @@ pub const FileExt = enum {
47475492 .static_library => target.staticLibSuffix(),
47485493 .zig => ".zig",
47495494 .def => ".def",
5495 .rc => ".rc",
47505496 .res => ".res",
47515497 .unknown => "",
47525498 };
......@@ -4839,7 +5585,9 @@ pub fn classifyFileExt(filename: []const u8) FileExt {
48395585 return .cu;
48405586 } else if (mem.endsWith(u8, filename, ".def")) {
48415587 return .def;
4842 } else if (mem.endsWith(u8, filename, ".res")) {
5588 } else if (std.ascii.endsWithIgnoreCase(filename, ".rc")) {
5589 return .rc;
5590 } else if (std.ascii.endsWithIgnoreCase(filename, ".res")) {
48435591 return .res;
48445592 } else {
48455593 return .unknown;
......@@ -4983,6 +5731,13 @@ fn detectLibCFromLibCInstallation(arena: Allocator, target: Target, lci: *const
49835731 if (!is_redundant) list.appendAssumeCapacity(lci.sys_include_dir.?);
49845732
49855733 if (target.os.tag == .windows) {
5734 if (std.fs.path.dirname(lci.sys_include_dir.?)) |sys_include_dir_parent| {
5735 // This include path will only exist when the optional "Desktop development with C++"
5736 // is installed. It contains headers, .rc files, and resources. It is especially
5737 // necessary when working with Windows resources.
5738 const atlmfc_dir = try std.fs.path.join(arena, &[_][]const u8{ sys_include_dir_parent, "atlmfc", "include" });
5739 list.appendAssumeCapacity(atlmfc_dir);
5740 }
49865741 if (std.fs.path.dirname(lci.include_dir.?)) |include_dir_parent| {
49875742 const um_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_parent, "um" });
49885743 list.appendAssumeCapacity(um_dir);
src/link.zig+7-1
......@@ -1027,6 +1027,9 @@ pub const File = struct {
10271027 for (comp.c_object_table.keys()) |key| {
10281028 _ = try man.addFile(key.status.success.object_path, null);
10291029 }
1030 for (comp.win32_resource_table.keys()) |key| {
1031 _ = try man.addFile(key.status.success.res_path, null);
1032 }
10301033 try man.addOptionalFile(module_obj_path);
10311034 try man.addOptionalFile(compiler_rt_path);
10321035
......@@ -1056,7 +1059,7 @@ pub const File = struct {
10561059 };
10571060 }
10581061
1059 const num_object_files = base.options.objects.len + comp.c_object_table.count() + 2;
1062 const num_object_files = base.options.objects.len + comp.c_object_table.count() + comp.win32_resource_table.count() + 2;
10601063 var object_files = try std.ArrayList([*:0]const u8).initCapacity(base.allocator, num_object_files);
10611064 defer object_files.deinit();
10621065
......@@ -1066,6 +1069,9 @@ pub const File = struct {
10661069 for (comp.c_object_table.keys()) |key| {
10671070 object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.object_path));
10681071 }
1072 for (comp.win32_resource_table.keys()) |key| {
1073 object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
1074 }
10691075 if (module_obj_path) |p| {
10701076 object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
10711077 }
src/link/Coff/lld.zig+7
......@@ -72,6 +72,9 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod
7272 for (comp.c_object_table.keys()) |key| {
7373 _ = try man.addFile(key.status.success.object_path, null);
7474 }
75 for (comp.win32_resource_table.keys()) |key| {
76 _ = try man.addFile(key.status.success.res_path, null);
77 }
7578 try man.addOptionalFile(module_obj_path);
7679 man.hash.addOptionalBytes(self.base.options.entry);
7780 man.hash.addOptional(self.base.options.stack_size_override);
......@@ -268,6 +271,10 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod
268271 try argv.append(key.status.success.object_path);
269272 }
270273
274 for (comp.win32_resource_table.keys()) |key| {
275 try argv.append(key.status.success.res_path);
276 }
277
271278 if (module_obj_path) |p| {
272279 try argv.append(p);
273280 }
src/main.zig+68-2
......@@ -472,6 +472,12 @@ const usage_build_generic =
472472 \\ -D[macro]=[value] Define C [macro] to [value] (1 if [value] omitted)
473473 \\ --libc [file] Provide a file which specifies libc paths
474474 \\ -cflags [flags] -- Set extra flags for the next positional C source files
475 \\ -rcflags [flags] -- Set extra flags for the next positional .rc source files
476 \\ -rcincludes=[type] Set the type of includes to use when compiling .rc source files
477 \\ any (default) Use msvc if available, fall back to gnu
478 \\ msvc Use msvc include paths (must be present on the system)
479 \\ gnu Use mingw include paths (distributed with Zig)
480 \\ none Do not use any autodetected include paths
475481 \\
476482 \\Link Options:
477483 \\ -l[lib], --library [lib] Link against system library (only if actually used)
......@@ -919,11 +925,15 @@ fn buildOutputType(
919925 var wasi_emulated_libs = std.ArrayList(wasi_libc.CRTFile).init(arena);
920926 var clang_argv = std.ArrayList([]const u8).init(arena);
921927 var extra_cflags = std.ArrayList([]const u8).init(arena);
928 var extra_rcflags = std.ArrayList([]const u8).init(arena);
922929 // These are before resolving sysroot.
923930 var lib_dir_args = std.ArrayList([]const u8).init(arena);
924931 var rpath_list = std.ArrayList([]const u8).init(arena);
925932 var symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .{};
926933 var c_source_files = std.ArrayList(Compilation.CSourceFile).init(arena);
934 var rc_source_files = std.ArrayList(Compilation.RcSourceFile).init(arena);
935 var rc_includes: Compilation.RcIncludes = .any;
936 var res_files = std.ArrayList(Compilation.LinkObject).init(arena);
927937 var link_objects = std.ArrayList(Compilation.LinkObject).init(arena);
928938 var framework_dirs = std.ArrayList([]const u8).init(arena);
929939 var frameworks: std.StringArrayHashMapUnmanaged(Framework) = .{};
......@@ -1042,6 +1052,19 @@ fn buildOutputType(
10421052 if (mem.eql(u8, next_arg, "--")) break;
10431053 try extra_cflags.append(next_arg);
10441054 }
1055 } else if (mem.eql(u8, arg, "-rcincludes")) {
1056 rc_includes = parseRcIncludes(args_iter.nextOrFatal());
1057 } else if (mem.startsWith(u8, arg, "-rcincludes=")) {
1058 rc_includes = parseRcIncludes(arg["-rcincludes=".len..]);
1059 } else if (mem.eql(u8, arg, "-rcflags")) {
1060 extra_rcflags.shrinkRetainingCapacity(0);
1061 while (true) {
1062 const next_arg = args_iter.next() orelse {
1063 fatal("expected -- after -rcflags", .{});
1064 };
1065 if (mem.eql(u8, next_arg, "--")) break;
1066 try extra_rcflags.append(next_arg);
1067 }
10451068 } else if (mem.eql(u8, arg, "--color")) {
10461069 const next_arg = args_iter.next() orelse {
10471070 fatal("expected [auto|on|off] after --color", .{});
......@@ -1590,7 +1613,8 @@ fn buildOutputType(
15901613 }
15911614 } else switch (file_ext orelse
15921615 Compilation.classifyFileExt(arg)) {
1593 .object, .static_library, .shared_library, .res => try link_objects.append(.{ .path = arg }),
1616 .object, .static_library, .shared_library => try link_objects.append(.{ .path = arg }),
1617 .res => try res_files.append(.{ .path = arg }),
15941618 .assembly, .assembly_with_cpp, .c, .cpp, .h, .ll, .bc, .m, .mm, .cu => {
15951619 try c_source_files.append(.{
15961620 .src_path = arg,
......@@ -1599,6 +1623,12 @@ fn buildOutputType(
15991623 .ext = file_ext,
16001624 });
16011625 },
1626 .rc => {
1627 try rc_source_files.append(.{
1628 .src_path = arg,
1629 .extra_flags = try arena.dupe([]const u8, extra_rcflags.items),
1630 });
1631 },
16021632 .zig => {
16031633 if (root_src_file) |other| {
16041634 fatal("found another zig file '{s}' after root source file '{s}'", .{ arg, other });
......@@ -1684,13 +1714,20 @@ fn buildOutputType(
16841714 .ext = file_ext, // duped while parsing the args.
16851715 });
16861716 },
1687 .unknown, .shared_library, .object, .static_library, .res => try link_objects.append(.{
1717 .unknown, .shared_library, .object, .static_library => try link_objects.append(.{
1718 .path = it.only_arg,
1719 .must_link = must_link,
1720 }),
1721 .res => try res_files.append(.{
16881722 .path = it.only_arg,
16891723 .must_link = must_link,
16901724 }),
16911725 .def => {
16921726 linker_module_definition_file = it.only_arg;
16931727 },
1728 .rc => {
1729 try rc_source_files.append(.{ .src_path = it.only_arg });
1730 },
16941731 .zig => {
16951732 if (root_src_file) |other| {
16961733 fatal("found another zig file '{s}' after root source file '{s}'", .{ it.only_arg, other });
......@@ -2452,6 +2489,12 @@ fn buildOutputType(
24522489 } else if (emit_bin == .yes) {
24532490 const basename = fs.path.basename(emit_bin.yes);
24542491 break :blk basename[0 .. basename.len - fs.path.extension(basename).len];
2492 } else if (rc_source_files.items.len >= 1) {
2493 const basename = fs.path.basename(rc_source_files.items[0].src_path);
2494 break :blk basename[0 .. basename.len - fs.path.extension(basename).len];
2495 } else if (res_files.items.len >= 1) {
2496 const basename = fs.path.basename(res_files.items[0].path);
2497 break :blk basename[0 .. basename.len - fs.path.extension(basename).len];
24552498 } else if (show_builtin) {
24562499 break :blk "builtin";
24572500 } else if (arg_mode == .run) {
......@@ -2530,6 +2573,21 @@ fn buildOutputType(
25302573 link_libcpp = true;
25312574 }
25322575
2576 if (target_info.target.ofmt == .coff) {
2577 // Now that we know the target supports resources,
2578 // we can add the res files as link objects.
2579 for (res_files.items) |res_file| {
2580 try link_objects.append(res_file);
2581 }
2582 } else {
2583 if (rc_source_files.items.len != 0) {
2584 fatal("rc files are not allowed unless the target object format is coff (Windows/UEFI)", .{});
2585 }
2586 if (res_files.items.len != 0) {
2587 fatal("res files are not allowed unless the target object format is coff (Windows/UEFI)", .{});
2588 }
2589 }
2590
25332591 if (target_info.target.cpu.arch.isWasm()) blk: {
25342592 if (single_threaded == null) {
25352593 single_threaded = true;
......@@ -2933,6 +2991,7 @@ fn buildOutputType(
29332991 if (output_mode == .Obj and (object_format == .coff or object_format == .macho)) {
29342992 const total_obj_count = c_source_files.items.len +
29352993 @intFromBool(root_src_file != null) +
2994 rc_source_files.items.len +
29362995 link_objects.items.len;
29372996 if (total_obj_count > 1) {
29382997 fatal("{s} does not support linking multiple objects into one", .{@tagName(object_format)});
......@@ -3319,6 +3378,8 @@ fn buildOutputType(
33193378 .rpath_list = rpath_list.items,
33203379 .symbol_wrap_set = symbol_wrap_set,
33213380 .c_source_files = c_source_files.items,
3381 .rc_source_files = rc_source_files.items,
3382 .rc_includes = rc_includes,
33223383 .link_objects = link_objects.items,
33233384 .framework_dirs = framework_dirs.items,
33243385 .frameworks = resolved_frameworks.items,
......@@ -6478,3 +6539,8 @@ fn accessFrameworkPath(
64786539
64796540 return false;
64806541}
6542
6543fn parseRcIncludes(arg: []const u8) Compilation.RcIncludes {
6544 return std.meta.stringToEnum(Compilation.RcIncludes, arg) orelse
6545 fatal("unsupported rc includes type: '{s}'", .{arg});
6546}
src/resinator.zig created+18
......@@ -0,0 +1,18 @@
1pub const ani = @import("resinator/ani.zig");
2pub const ast = @import("resinator/ast.zig");
3pub const bmp = @import("resinator/bmp.zig");
4pub const cli = @import("resinator/cli.zig");
5pub const code_pages = @import("resinator/code_pages.zig");
6pub const comments = @import("resinator/comments.zig");
7pub const compile = @import("resinator/compile.zig");
8pub const errors = @import("resinator/errors.zig");
9pub const ico = @import("resinator/ico.zig");
10pub const lang = @import("resinator/lang.zig");
11pub const lex = @import("resinator/lex.zig");
12pub const literals = @import("resinator/literals.zig");
13pub const parse = @import("resinator/parse.zig");
14pub const rc = @import("resinator/rc.zig");
15pub const res = @import("resinator/res.zig");
16pub const source_mapping = @import("resinator/source_mapping.zig");
17pub const utils = @import("resinator/utils.zig");
18pub const windows1252 = @import("resinator/windows1252.zig");
src/resinator/ani.zig created+58
......@@ -0,0 +1,58 @@
1//! https://en.wikipedia.org/wiki/Resource_Interchange_File_Format
2//! https://www.moon-soft.com/program/format/windows/ani.htm
3//! https://www.gdgsoft.com/anituner/help/aniformat.htm
4//! https://www.lomont.org/software/aniexploit/ExploitANI.pdf
5//!
6//! RIFF( 'ACON'
7//! [LIST( 'INFO' <info_data> )]
8//! [<DISP_ck>]
9//! anih( <ani_header> )
10//! [rate( <rate_info> )]
11//! ['seq '( <sequence_info> )]
12//! LIST( 'fram' icon( <icon_file> ) ... )
13//! )
14
15const std = @import("std");
16
17const AF_ICON: u32 = 1;
18
19pub fn isAnimatedIcon(reader: anytype) bool {
20 const flags = getAniheaderFlags(reader) catch return false;
21 return flags & AF_ICON == AF_ICON;
22}
23
24fn getAniheaderFlags(reader: anytype) !u32 {
25 const riff_header = try reader.readBytesNoEof(4);
26 if (!std.mem.eql(u8, &riff_header, "RIFF")) return error.InvalidFormat;
27
28 _ = try reader.readIntLittle(u32); // size of RIFF chunk
29
30 const form_type = try reader.readBytesNoEof(4);
31 if (!std.mem.eql(u8, &form_type, "ACON")) return error.InvalidFormat;
32
33 while (true) {
34 const chunk_id = try reader.readBytesNoEof(4);
35 const chunk_len = try reader.readIntLittle(u32);
36 if (!std.mem.eql(u8, &chunk_id, "anih")) {
37 // TODO: Move file cursor instead of skipBytes
38 try reader.skipBytes(chunk_len, .{});
39 continue;
40 }
41
42 const aniheader = try reader.readStruct(ANIHEADER);
43 return std.mem.nativeToLittle(u32, aniheader.flags);
44 }
45}
46
47/// From Microsoft Multimedia Data Standards Update April 15, 1994
48const ANIHEADER = extern struct {
49 cbSizeof: u32,
50 cFrames: u32,
51 cSteps: u32,
52 cx: u32,
53 cy: u32,
54 cBitCount: u32,
55 cPlanes: u32,
56 jifRate: u32,
57 flags: u32,
58};
src/resinator/ast.zig created+1084
......@@ -0,0 +1,1084 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const Token = @import("lex.zig").Token;
4const CodePage = @import("code_pages.zig").CodePage;
5
6pub const Tree = struct {
7 node: *Node,
8 input_code_pages: CodePageLookup,
9 output_code_pages: CodePageLookup,
10
11 /// not owned by the tree
12 source: []const u8,
13
14 arena: std.heap.ArenaAllocator.State,
15 allocator: Allocator,
16
17 pub fn deinit(self: *Tree) void {
18 self.arena.promote(self.allocator).deinit();
19 }
20
21 pub fn root(self: *Tree) *Node.Root {
22 return @fieldParentPtr(Node.Root, "base", self.node);
23 }
24
25 pub fn dump(self: *Tree, writer: anytype) @TypeOf(writer).Error!void {
26 try self.node.dump(self, writer, 0);
27 }
28};
29
30pub const CodePageLookup = struct {
31 lookup: std.ArrayListUnmanaged(CodePage) = .{},
32 allocator: Allocator,
33 default_code_page: CodePage,
34
35 pub fn init(allocator: Allocator, default_code_page: CodePage) CodePageLookup {
36 return .{
37 .allocator = allocator,
38 .default_code_page = default_code_page,
39 };
40 }
41
42 pub fn deinit(self: *CodePageLookup) void {
43 self.lookup.deinit(self.allocator);
44 }
45
46 /// line_num is 1-indexed
47 pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: CodePage) !void {
48 const index = line_num - 1;
49 if (index >= self.lookup.items.len) {
50 const new_size = line_num;
51 const missing_lines_start_index = self.lookup.items.len;
52 try self.lookup.resize(self.allocator, new_size);
53
54 // If there are any gaps created, we need to fill them in with the value of the
55 // last line before the gap. This can happen for e.g. string literals that
56 // span multiple lines, or if the start of a file has multiple empty lines.
57 const fill_value = if (missing_lines_start_index > 0)
58 self.lookup.items[missing_lines_start_index - 1]
59 else
60 self.default_code_page;
61 var i: usize = missing_lines_start_index;
62 while (i < new_size - 1) : (i += 1) {
63 self.lookup.items[i] = fill_value;
64 }
65 }
66 self.lookup.items[index] = code_page;
67 }
68
69 pub fn setForToken(self: *CodePageLookup, token: Token, code_page: CodePage) !void {
70 return self.setForLineNum(token.line_number, code_page);
71 }
72
73 /// line_num is 1-indexed
74 pub fn getForLineNum(self: CodePageLookup, line_num: usize) CodePage {
75 return self.lookup.items[line_num - 1];
76 }
77
78 pub fn getForToken(self: CodePageLookup, token: Token) CodePage {
79 return self.getForLineNum(token.line_number);
80 }
81};
82
83test "CodePageLookup" {
84 var lookup = CodePageLookup.init(std.testing.allocator, .windows1252);
85 defer lookup.deinit();
86
87 try lookup.setForLineNum(5, .utf8);
88 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));
89 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));
90 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));
91 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));
92 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));
93 try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len);
94
95 try lookup.setForLineNum(7, .windows1252);
96 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));
97 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));
98 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));
99 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));
100 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));
101 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(6));
102 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(7));
103 try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len);
104}
105
106pub const Node = struct {
107 id: Id,
108
109 pub const Id = enum {
110 root,
111 resource_external,
112 resource_raw_data,
113 literal,
114 binary_expression,
115 grouped_expression,
116 not_expression,
117 accelerators,
118 accelerator,
119 dialog,
120 control_statement,
121 toolbar,
122 menu,
123 menu_item,
124 menu_item_separator,
125 menu_item_ex,
126 popup,
127 popup_ex,
128 version_info,
129 version_statement,
130 block,
131 block_value,
132 block_value_value,
133 string_table,
134 string_table_string,
135 language_statement,
136 font_statement,
137 simple_statement,
138 invalid,
139
140 pub fn Type(comptime id: Id) type {
141 return switch (id) {
142 .root => Root,
143 .resource_external => ResourceExternal,
144 .resource_raw_data => ResourceRawData,
145 .literal => Literal,
146 .binary_expression => BinaryExpression,
147 .grouped_expression => GroupedExpression,
148 .not_expression => NotExpression,
149 .accelerators => Accelerators,
150 .accelerator => Accelerator,
151 .dialog => Dialog,
152 .control_statement => ControlStatement,
153 .toolbar => Toolbar,
154 .menu => Menu,
155 .menu_item => MenuItem,
156 .menu_item_separator => MenuItemSeparator,
157 .menu_item_ex => MenuItemEx,
158 .popup => Popup,
159 .popup_ex => PopupEx,
160 .version_info => VersionInfo,
161 .version_statement => VersionStatement,
162 .block => Block,
163 .block_value => BlockValue,
164 .block_value_value => BlockValueValue,
165 .string_table => StringTable,
166 .string_table_string => StringTableString,
167 .language_statement => LanguageStatement,
168 .font_statement => FontStatement,
169 .simple_statement => SimpleStatement,
170 .invalid => Invalid,
171 };
172 }
173 };
174
175 pub fn cast(base: *Node, comptime id: Id) ?*id.Type() {
176 if (base.id == id) {
177 return @fieldParentPtr(id.Type(), "base", base);
178 }
179 return null;
180 }
181
182 pub const Root = struct {
183 base: Node = .{ .id = .root },
184 body: []*Node,
185 };
186
187 pub const ResourceExternal = struct {
188 base: Node = .{ .id = .resource_external },
189 id: Token,
190 type: Token,
191 common_resource_attributes: []Token,
192 filename: *Node,
193 };
194
195 pub const ResourceRawData = struct {
196 base: Node = .{ .id = .resource_raw_data },
197 id: Token,
198 type: Token,
199 common_resource_attributes: []Token,
200 begin_token: Token,
201 raw_data: []*Node,
202 end_token: Token,
203 };
204
205 pub const Literal = struct {
206 base: Node = .{ .id = .literal },
207 token: Token,
208 };
209
210 pub const BinaryExpression = struct {
211 base: Node = .{ .id = .binary_expression },
212 operator: Token,
213 left: *Node,
214 right: *Node,
215 };
216
217 pub const GroupedExpression = struct {
218 base: Node = .{ .id = .grouped_expression },
219 open_token: Token,
220 expression: *Node,
221 close_token: Token,
222 };
223
224 pub const NotExpression = struct {
225 base: Node = .{ .id = .not_expression },
226 not_token: Token,
227 number_token: Token,
228 };
229
230 pub const Accelerators = struct {
231 base: Node = .{ .id = .accelerators },
232 id: Token,
233 type: Token,
234 common_resource_attributes: []Token,
235 optional_statements: []*Node,
236 begin_token: Token,
237 accelerators: []*Node,
238 end_token: Token,
239 };
240
241 pub const Accelerator = struct {
242 base: Node = .{ .id = .accelerator },
243 event: *Node,
244 idvalue: *Node,
245 type_and_options: []Token,
246 };
247
248 pub const Dialog = struct {
249 base: Node = .{ .id = .dialog },
250 id: Token,
251 type: Token,
252 common_resource_attributes: []Token,
253 x: *Node,
254 y: *Node,
255 width: *Node,
256 height: *Node,
257 help_id: ?*Node,
258 optional_statements: []*Node,
259 begin_token: Token,
260 controls: []*Node,
261 end_token: Token,
262 };
263
264 pub const ControlStatement = struct {
265 base: Node = .{ .id = .control_statement },
266 type: Token,
267 text: ?Token,
268 /// Only relevant for the user-defined CONTROL control
269 class: ?*Node,
270 id: *Node,
271 x: *Node,
272 y: *Node,
273 width: *Node,
274 height: *Node,
275 style: ?*Node,
276 exstyle: ?*Node,
277 help_id: ?*Node,
278 extra_data_begin: ?Token,
279 extra_data: []*Node,
280 extra_data_end: ?Token,
281
282 /// Returns true if this node describes a user-defined CONTROL control
283 /// https://learn.microsoft.com/en-us/windows/win32/menurc/control-control
284 pub fn isUserDefined(self: *const ControlStatement) bool {
285 return self.class != null;
286 }
287 };
288
289 pub const Toolbar = struct {
290 base: Node = .{ .id = .toolbar },
291 id: Token,
292 type: Token,
293 common_resource_attributes: []Token,
294 button_width: *Node,
295 button_height: *Node,
296 begin_token: Token,
297 /// Will contain Literal and SimpleStatement nodes
298 buttons: []*Node,
299 end_token: Token,
300 };
301
302 pub const Menu = struct {
303 base: Node = .{ .id = .menu },
304 id: Token,
305 type: Token,
306 common_resource_attributes: []Token,
307 optional_statements: []*Node,
308 /// `help_id` will never be non-null if `type` is MENU
309 help_id: ?*Node,
310 begin_token: Token,
311 items: []*Node,
312 end_token: Token,
313 };
314
315 pub const MenuItem = struct {
316 base: Node = .{ .id = .menu_item },
317 menuitem: Token,
318 text: Token,
319 result: *Node,
320 option_list: []Token,
321 };
322
323 pub const MenuItemSeparator = struct {
324 base: Node = .{ .id = .menu_item_separator },
325 menuitem: Token,
326 separator: Token,
327 };
328
329 pub const MenuItemEx = struct {
330 base: Node = .{ .id = .menu_item_ex },
331 menuitem: Token,
332 text: Token,
333 id: ?*Node,
334 type: ?*Node,
335 state: ?*Node,
336 };
337
338 pub const Popup = struct {
339 base: Node = .{ .id = .popup },
340 popup: Token,
341 text: Token,
342 option_list: []Token,
343 begin_token: Token,
344 items: []*Node,
345 end_token: Token,
346 };
347
348 pub const PopupEx = struct {
349 base: Node = .{ .id = .popup_ex },
350 popup: Token,
351 text: Token,
352 id: ?*Node,
353 type: ?*Node,
354 state: ?*Node,
355 help_id: ?*Node,
356 begin_token: Token,
357 items: []*Node,
358 end_token: Token,
359 };
360
361 pub const VersionInfo = struct {
362 base: Node = .{ .id = .version_info },
363 id: Token,
364 versioninfo: Token,
365 common_resource_attributes: []Token,
366 /// Will contain VersionStatement and/or SimpleStatement nodes
367 fixed_info: []*Node,
368 begin_token: Token,
369 block_statements: []*Node,
370 end_token: Token,
371 };
372
373 /// Used for FILEVERSION and PRODUCTVERSION statements
374 pub const VersionStatement = struct {
375 base: Node = .{ .id = .version_statement },
376 type: Token,
377 /// Between 1-4 parts
378 parts: []*Node,
379 };
380
381 pub const Block = struct {
382 base: Node = .{ .id = .block },
383 /// The BLOCK token itself
384 identifier: Token,
385 key: Token,
386 /// This is undocumented but BLOCK statements support values after
387 /// the key just like VALUE statements.
388 values: []*Node,
389 begin_token: Token,
390 children: []*Node,
391 end_token: Token,
392 };
393
394 pub const BlockValue = struct {
395 base: Node = .{ .id = .block_value },
396 /// The VALUE token itself
397 identifier: Token,
398 key: Token,
399 /// These will be BlockValueValue nodes
400 values: []*Node,
401 };
402
403 pub const BlockValueValue = struct {
404 base: Node = .{ .id = .block_value_value },
405 expression: *Node,
406 /// Whether or not the value has a trailing comma is relevant
407 trailing_comma: bool,
408 };
409
410 pub const StringTable = struct {
411 base: Node = .{ .id = .string_table },
412 type: Token,
413 common_resource_attributes: []Token,
414 optional_statements: []*Node,
415 begin_token: Token,
416 strings: []*Node,
417 end_token: Token,
418 };
419
420 pub const StringTableString = struct {
421 base: Node = .{ .id = .string_table_string },
422 id: *Node,
423 maybe_comma: ?Token,
424 string: Token,
425 };
426
427 pub const LanguageStatement = struct {
428 base: Node = .{ .id = .language_statement },
429 /// The LANGUAGE token itself
430 language_token: Token,
431 primary_language_id: *Node,
432 sublanguage_id: *Node,
433 };
434
435 pub const FontStatement = struct {
436 base: Node = .{ .id = .font_statement },
437 /// The FONT token itself
438 identifier: Token,
439 point_size: *Node,
440 typeface: Token,
441 weight: ?*Node,
442 italic: ?*Node,
443 char_set: ?*Node,
444 };
445
446 /// A statement with one value associated with it.
447 /// Used for CAPTION, CHARACTERISTICS, CLASS, EXSTYLE, MENU, STYLE, VERSION,
448 /// as well as VERSIONINFO-specific statements FILEFLAGSMASK, FILEFLAGS, FILEOS,
449 /// FILETYPE, FILESUBTYPE
450 pub const SimpleStatement = struct {
451 base: Node = .{ .id = .simple_statement },
452 identifier: Token,
453 value: *Node,
454 };
455
456 pub const Invalid = struct {
457 base: Node = .{ .id = .invalid },
458 context: []Token,
459 };
460
461 pub fn isNumberExpression(node: *const Node) bool {
462 switch (node.id) {
463 .literal => {
464 const literal = @fieldParentPtr(Node.Literal, "base", node);
465 return switch (literal.token.id) {
466 .number => true,
467 else => false,
468 };
469 },
470 .binary_expression, .grouped_expression, .not_expression => return true,
471 else => return false,
472 }
473 }
474
475 pub fn isStringLiteral(node: *const Node) bool {
476 switch (node.id) {
477 .literal => {
478 const literal = @fieldParentPtr(Node.Literal, "base", node);
479 return switch (literal.token.id) {
480 .quoted_ascii_string, .quoted_wide_string => true,
481 else => false,
482 };
483 },
484 else => return false,
485 }
486 }
487
488 pub fn getFirstToken(node: *const Node) Token {
489 switch (node.id) {
490 .root => unreachable,
491 .resource_external => {
492 const casted = @fieldParentPtr(Node.ResourceExternal, "base", node);
493 return casted.id;
494 },
495 .resource_raw_data => {
496 const casted = @fieldParentPtr(Node.ResourceRawData, "base", node);
497 return casted.id;
498 },
499 .literal => {
500 const casted = @fieldParentPtr(Node.Literal, "base", node);
501 return casted.token;
502 },
503 .binary_expression => {
504 const casted = @fieldParentPtr(Node.BinaryExpression, "base", node);
505 return casted.left.getFirstToken();
506 },
507 .grouped_expression => {
508 const casted = @fieldParentPtr(Node.GroupedExpression, "base", node);
509 return casted.open_token;
510 },
511 .not_expression => {
512 const casted = @fieldParentPtr(Node.NotExpression, "base", node);
513 return casted.not_token;
514 },
515 .accelerators => {
516 const casted = @fieldParentPtr(Node.Accelerators, "base", node);
517 return casted.id;
518 },
519 .accelerator => {
520 const casted = @fieldParentPtr(Node.Accelerator, "base", node);
521 return casted.event.getFirstToken();
522 },
523 .dialog => {
524 const casted = @fieldParentPtr(Node.Dialog, "base", node);
525 return casted.id;
526 },
527 .control_statement => {
528 const casted = @fieldParentPtr(Node.ControlStatement, "base", node);
529 return casted.type;
530 },
531 .toolbar => {
532 const casted = @fieldParentPtr(Node.Toolbar, "base", node);
533 return casted.id;
534 },
535 .menu => {
536 const casted = @fieldParentPtr(Node.Menu, "base", node);
537 return casted.id;
538 },
539 inline .menu_item, .menu_item_separator, .menu_item_ex => |menu_item_type| {
540 const node_type = menu_item_type.Type();
541 const casted = @fieldParentPtr(node_type, "base", node);
542 return casted.menuitem;
543 },
544 inline .popup, .popup_ex => |popup_type| {
545 const node_type = popup_type.Type();
546 const casted = @fieldParentPtr(node_type, "base", node);
547 return casted.popup;
548 },
549 .version_info => {
550 const casted = @fieldParentPtr(Node.VersionInfo, "base", node);
551 return casted.id;
552 },
553 .version_statement => {
554 const casted = @fieldParentPtr(Node.VersionStatement, "base", node);
555 return casted.type;
556 },
557 .block => {
558 const casted = @fieldParentPtr(Node.Block, "base", node);
559 return casted.identifier;
560 },
561 .block_value => {
562 const casted = @fieldParentPtr(Node.BlockValue, "base", node);
563 return casted.identifier;
564 },
565 .block_value_value => {
566 const casted = @fieldParentPtr(Node.BlockValueValue, "base", node);
567 return casted.expression.getFirstToken();
568 },
569 .string_table => {
570 const casted = @fieldParentPtr(Node.StringTable, "base", node);
571 return casted.type;
572 },
573 .string_table_string => {
574 const casted = @fieldParentPtr(Node.StringTableString, "base", node);
575 return casted.id.getFirstToken();
576 },
577 .language_statement => {
578 const casted = @fieldParentPtr(Node.LanguageStatement, "base", node);
579 return casted.language_token;
580 },
581 .font_statement => {
582 const casted = @fieldParentPtr(Node.FontStatement, "base", node);
583 return casted.identifier;
584 },
585 .simple_statement => {
586 const casted = @fieldParentPtr(Node.SimpleStatement, "base", node);
587 return casted.identifier;
588 },
589 .invalid => {
590 const casted = @fieldParentPtr(Node.Invalid, "base", node);
591 return casted.context[0];
592 },
593 }
594 }
595
596 pub fn getLastToken(node: *const Node) Token {
597 switch (node.id) {
598 .root => unreachable,
599 .resource_external => {
600 const casted = @fieldParentPtr(Node.ResourceExternal, "base", node);
601 return casted.filename.getLastToken();
602 },
603 .resource_raw_data => {
604 const casted = @fieldParentPtr(Node.ResourceRawData, "base", node);
605 return casted.end_token;
606 },
607 .literal => {
608 const casted = @fieldParentPtr(Node.Literal, "base", node);
609 return casted.token;
610 },
611 .binary_expression => {
612 const casted = @fieldParentPtr(Node.BinaryExpression, "base", node);
613 return casted.right.getLastToken();
614 },
615 .grouped_expression => {
616 const casted = @fieldParentPtr(Node.GroupedExpression, "base", node);
617 return casted.close_token;
618 },
619 .not_expression => {
620 const casted = @fieldParentPtr(Node.NotExpression, "base", node);
621 return casted.number_token;
622 },
623 .accelerators => {
624 const casted = @fieldParentPtr(Node.Accelerators, "base", node);
625 return casted.end_token;
626 },
627 .accelerator => {
628 const casted = @fieldParentPtr(Node.Accelerator, "base", node);
629 if (casted.type_and_options.len > 0) return casted.type_and_options[casted.type_and_options.len - 1];
630 return casted.idvalue.getLastToken();
631 },
632 .dialog => {
633 const casted = @fieldParentPtr(Node.Dialog, "base", node);
634 return casted.end_token;
635 },
636 .control_statement => {
637 const casted = @fieldParentPtr(Node.ControlStatement, "base", node);
638 if (casted.extra_data_end) |token| return token;
639 if (casted.help_id) |help_id_node| return help_id_node.getLastToken();
640 if (casted.exstyle) |exstyle_node| return exstyle_node.getLastToken();
641 // For user-defined CONTROL controls, the style comes before 'x', but
642 // otherwise it comes after 'height' so it could be the last token if
643 // it's present.
644 if (!casted.isUserDefined()) {
645 if (casted.style) |style_node| return style_node.getLastToken();
646 }
647 return casted.height.getLastToken();
648 },
649 .toolbar => {
650 const casted = @fieldParentPtr(Node.Toolbar, "base", node);
651 return casted.end_token;
652 },
653 .menu => {
654 const casted = @fieldParentPtr(Node.Menu, "base", node);
655 return casted.end_token;
656 },
657 .menu_item => {
658 const casted = @fieldParentPtr(Node.MenuItem, "base", node);
659 if (casted.option_list.len > 0) return casted.option_list[casted.option_list.len - 1];
660 return casted.result.getLastToken();
661 },
662 .menu_item_separator => {
663 const casted = @fieldParentPtr(Node.MenuItemSeparator, "base", node);
664 return casted.separator;
665 },
666 .menu_item_ex => {
667 const casted = @fieldParentPtr(Node.MenuItemEx, "base", node);
668 if (casted.state) |state_node| return state_node.getLastToken();
669 if (casted.type) |type_node| return type_node.getLastToken();
670 if (casted.id) |id_node| return id_node.getLastToken();
671 return casted.text;
672 },
673 inline .popup, .popup_ex => |popup_type| {
674 const node_type = popup_type.Type();
675 const casted = @fieldParentPtr(node_type, "base", node);
676 return casted.end_token;
677 },
678 .version_info => {
679 const casted = @fieldParentPtr(Node.VersionInfo, "base", node);
680 return casted.end_token;
681 },
682 .version_statement => {
683 const casted = @fieldParentPtr(Node.VersionStatement, "base", node);
684 return casted.parts[casted.parts.len - 1].getLastToken();
685 },
686 .block => {
687 const casted = @fieldParentPtr(Node.Block, "base", node);
688 return casted.end_token;
689 },
690 .block_value => {
691 const casted = @fieldParentPtr(Node.BlockValue, "base", node);
692 if (casted.values.len > 0) return casted.values[casted.values.len - 1].getLastToken();
693 return casted.key;
694 },
695 .block_value_value => {
696 const casted = @fieldParentPtr(Node.BlockValueValue, "base", node);
697 return casted.expression.getLastToken();
698 },
699 .string_table => {
700 const casted = @fieldParentPtr(Node.StringTable, "base", node);
701 return casted.end_token;
702 },
703 .string_table_string => {
704 const casted = @fieldParentPtr(Node.StringTableString, "base", node);
705 return casted.string;
706 },
707 .language_statement => {
708 const casted = @fieldParentPtr(Node.LanguageStatement, "base", node);
709 return casted.sublanguage_id.getLastToken();
710 },
711 .font_statement => {
712 const casted = @fieldParentPtr(Node.FontStatement, "base", node);
713 if (casted.char_set) |char_set_node| return char_set_node.getLastToken();
714 if (casted.italic) |italic_node| return italic_node.getLastToken();
715 if (casted.weight) |weight_node| return weight_node.getLastToken();
716 return casted.typeface;
717 },
718 .simple_statement => {
719 const casted = @fieldParentPtr(Node.SimpleStatement, "base", node);
720 return casted.value.getLastToken();
721 },
722 .invalid => {
723 const casted = @fieldParentPtr(Node.Invalid, "base", node);
724 return casted.context[casted.context.len - 1];
725 },
726 }
727 }
728
729 pub fn dump(
730 node: *const Node,
731 tree: *const Tree,
732 writer: anytype,
733 indent: usize,
734 ) @TypeOf(writer).Error!void {
735 try writer.writeByteNTimes(' ', indent);
736 try writer.writeAll(@tagName(node.id));
737 switch (node.id) {
738 .root => {
739 try writer.writeAll("\n");
740 const root = @fieldParentPtr(Node.Root, "base", node);
741 for (root.body) |body_node| {
742 try body_node.dump(tree, writer, indent + 1);
743 }
744 },
745 .resource_external => {
746 const resource = @fieldParentPtr(Node.ResourceExternal, "base", node);
747 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len });
748 try resource.filename.dump(tree, writer, indent + 1);
749 },
750 .resource_raw_data => {
751 const resource = @fieldParentPtr(Node.ResourceRawData, "base", node);
752 try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len });
753 for (resource.raw_data) |data_expression| {
754 try data_expression.dump(tree, writer, indent + 1);
755 }
756 },
757 .literal => {
758 const literal = @fieldParentPtr(Node.Literal, "base", node);
759 try writer.writeAll(" ");
760 try writer.writeAll(literal.token.slice(tree.source));
761 try writer.writeAll("\n");
762 },
763 .binary_expression => {
764 const binary = @fieldParentPtr(Node.BinaryExpression, "base", node);
765 try writer.writeAll(" ");
766 try writer.writeAll(binary.operator.slice(tree.source));
767 try writer.writeAll("\n");
768 try binary.left.dump(tree, writer, indent + 1);
769 try binary.right.dump(tree, writer, indent + 1);
770 },
771 .grouped_expression => {
772 const grouped = @fieldParentPtr(Node.GroupedExpression, "base", node);
773 try writer.writeAll("\n");
774 try writer.writeByteNTimes(' ', indent);
775 try writer.writeAll(grouped.open_token.slice(tree.source));
776 try writer.writeAll("\n");
777 try grouped.expression.dump(tree, writer, indent + 1);
778 try writer.writeByteNTimes(' ', indent);
779 try writer.writeAll(grouped.close_token.slice(tree.source));
780 try writer.writeAll("\n");
781 },
782 .not_expression => {
783 const not = @fieldParentPtr(Node.NotExpression, "base", node);
784 try writer.writeAll(" ");
785 try writer.writeAll(not.not_token.slice(tree.source));
786 try writer.writeAll(" ");
787 try writer.writeAll(not.number_token.slice(tree.source));
788 try writer.writeAll("\n");
789 },
790 .accelerators => {
791 const accelerators = @fieldParentPtr(Node.Accelerators, "base", node);
792 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len });
793 for (accelerators.optional_statements) |statement| {
794 try statement.dump(tree, writer, indent + 1);
795 }
796 try writer.writeByteNTimes(' ', indent);
797 try writer.writeAll(accelerators.begin_token.slice(tree.source));
798 try writer.writeAll("\n");
799 for (accelerators.accelerators) |accelerator| {
800 try accelerator.dump(tree, writer, indent + 1);
801 }
802 try writer.writeByteNTimes(' ', indent);
803 try writer.writeAll(accelerators.end_token.slice(tree.source));
804 try writer.writeAll("\n");
805 },
806 .accelerator => {
807 const accelerator = @fieldParentPtr(Node.Accelerator, "base", node);
808 for (accelerator.type_and_options, 0..) |option, i| {
809 if (i != 0) try writer.writeAll(",");
810 try writer.writeByte(' ');
811 try writer.writeAll(option.slice(tree.source));
812 }
813 try writer.writeAll("\n");
814 try accelerator.event.dump(tree, writer, indent + 1);
815 try accelerator.idvalue.dump(tree, writer, indent + 1);
816 },
817 .dialog => {
818 const dialog = @fieldParentPtr(Node.Dialog, "base", node);
819 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len });
820 inline for (.{ "x", "y", "width", "height" }) |arg| {
821 try writer.writeByteNTimes(' ', indent + 1);
822 try writer.writeAll(arg ++ ":\n");
823 try @field(dialog, arg).dump(tree, writer, indent + 2);
824 }
825 if (dialog.help_id) |help_id| {
826 try writer.writeByteNTimes(' ', indent + 1);
827 try writer.writeAll("help_id:\n");
828 try help_id.dump(tree, writer, indent + 2);
829 }
830 for (dialog.optional_statements) |statement| {
831 try statement.dump(tree, writer, indent + 1);
832 }
833 try writer.writeByteNTimes(' ', indent);
834 try writer.writeAll(dialog.begin_token.slice(tree.source));
835 try writer.writeAll("\n");
836 for (dialog.controls) |control| {
837 try control.dump(tree, writer, indent + 1);
838 }
839 try writer.writeByteNTimes(' ', indent);
840 try writer.writeAll(dialog.end_token.slice(tree.source));
841 try writer.writeAll("\n");
842 },
843 .control_statement => {
844 const control = @fieldParentPtr(Node.ControlStatement, "base", node);
845 try writer.print(" {s}", .{control.type.slice(tree.source)});
846 if (control.text) |text| {
847 try writer.print(" text: {s}", .{text.slice(tree.source)});
848 }
849 try writer.writeByte('\n');
850 if (control.class) |class| {
851 try writer.writeByteNTimes(' ', indent + 1);
852 try writer.writeAll("class:\n");
853 try class.dump(tree, writer, indent + 2);
854 }
855 inline for (.{ "id", "x", "y", "width", "height" }) |arg| {
856 try writer.writeByteNTimes(' ', indent + 1);
857 try writer.writeAll(arg ++ ":\n");
858 try @field(control, arg).dump(tree, writer, indent + 2);
859 }
860 inline for (.{ "style", "exstyle", "help_id" }) |arg| {
861 if (@field(control, arg)) |val_node| {
862 try writer.writeByteNTimes(' ', indent + 1);
863 try writer.writeAll(arg ++ ":\n");
864 try val_node.dump(tree, writer, indent + 2);
865 }
866 }
867 if (control.extra_data_begin != null) {
868 try writer.writeByteNTimes(' ', indent);
869 try writer.writeAll(control.extra_data_begin.?.slice(tree.source));
870 try writer.writeAll("\n");
871 for (control.extra_data) |data_node| {
872 try data_node.dump(tree, writer, indent + 1);
873 }
874 try writer.writeByteNTimes(' ', indent);
875 try writer.writeAll(control.extra_data_end.?.slice(tree.source));
876 try writer.writeAll("\n");
877 }
878 },
879 .toolbar => {
880 const toolbar = @fieldParentPtr(Node.Toolbar, "base", node);
881 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len });
882 inline for (.{ "button_width", "button_height" }) |arg| {
883 try writer.writeByteNTimes(' ', indent + 1);
884 try writer.writeAll(arg ++ ":\n");
885 try @field(toolbar, arg).dump(tree, writer, indent + 2);
886 }
887 try writer.writeByteNTimes(' ', indent);
888 try writer.writeAll(toolbar.begin_token.slice(tree.source));
889 try writer.writeAll("\n");
890 for (toolbar.buttons) |button_or_sep| {
891 try button_or_sep.dump(tree, writer, indent + 1);
892 }
893 try writer.writeByteNTimes(' ', indent);
894 try writer.writeAll(toolbar.end_token.slice(tree.source));
895 try writer.writeAll("\n");
896 },
897 .menu => {
898 const menu = @fieldParentPtr(Node.Menu, "base", node);
899 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len });
900 for (menu.optional_statements) |statement| {
901 try statement.dump(tree, writer, indent + 1);
902 }
903 if (menu.help_id) |help_id| {
904 try writer.writeByteNTimes(' ', indent + 1);
905 try writer.writeAll("help_id:\n");
906 try help_id.dump(tree, writer, indent + 2);
907 }
908 try writer.writeByteNTimes(' ', indent);
909 try writer.writeAll(menu.begin_token.slice(tree.source));
910 try writer.writeAll("\n");
911 for (menu.items) |item| {
912 try item.dump(tree, writer, indent + 1);
913 }
914 try writer.writeByteNTimes(' ', indent);
915 try writer.writeAll(menu.end_token.slice(tree.source));
916 try writer.writeAll("\n");
917 },
918 .menu_item => {
919 const menu_item = @fieldParentPtr(Node.MenuItem, "base", node);
920 try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len });
921 try menu_item.result.dump(tree, writer, indent + 1);
922 },
923 .menu_item_separator => {
924 const menu_item = @fieldParentPtr(Node.MenuItemSeparator, "base", node);
925 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) });
926 },
927 .menu_item_ex => {
928 const menu_item = @fieldParentPtr(Node.MenuItemEx, "base", node);
929 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) });
930 inline for (.{ "id", "type", "state" }) |arg| {
931 if (@field(menu_item, arg)) |val_node| {
932 try writer.writeByteNTimes(' ', indent + 1);
933 try writer.writeAll(arg ++ ":\n");
934 try val_node.dump(tree, writer, indent + 2);
935 }
936 }
937 },
938 .popup => {
939 const popup = @fieldParentPtr(Node.Popup, "base", node);
940 try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len });
941 try writer.writeByteNTimes(' ', indent);
942 try writer.writeAll(popup.begin_token.slice(tree.source));
943 try writer.writeAll("\n");
944 for (popup.items) |item| {
945 try item.dump(tree, writer, indent + 1);
946 }
947 try writer.writeByteNTimes(' ', indent);
948 try writer.writeAll(popup.end_token.slice(tree.source));
949 try writer.writeAll("\n");
950 },
951 .popup_ex => {
952 const popup = @fieldParentPtr(Node.PopupEx, "base", node);
953 try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) });
954 inline for (.{ "id", "type", "state", "help_id" }) |arg| {
955 if (@field(popup, arg)) |val_node| {
956 try writer.writeByteNTimes(' ', indent + 1);
957 try writer.writeAll(arg ++ ":\n");
958 try val_node.dump(tree, writer, indent + 2);
959 }
960 }
961 try writer.writeByteNTimes(' ', indent);
962 try writer.writeAll(popup.begin_token.slice(tree.source));
963 try writer.writeAll("\n");
964 for (popup.items) |item| {
965 try item.dump(tree, writer, indent + 1);
966 }
967 try writer.writeByteNTimes(' ', indent);
968 try writer.writeAll(popup.end_token.slice(tree.source));
969 try writer.writeAll("\n");
970 },
971 .version_info => {
972 const version_info = @fieldParentPtr(Node.VersionInfo, "base", node);
973 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len });
974 for (version_info.fixed_info) |fixed_info| {
975 try fixed_info.dump(tree, writer, indent + 1);
976 }
977 try writer.writeByteNTimes(' ', indent);
978 try writer.writeAll(version_info.begin_token.slice(tree.source));
979 try writer.writeAll("\n");
980 for (version_info.block_statements) |block| {
981 try block.dump(tree, writer, indent + 1);
982 }
983 try writer.writeByteNTimes(' ', indent);
984 try writer.writeAll(version_info.end_token.slice(tree.source));
985 try writer.writeAll("\n");
986 },
987 .version_statement => {
988 const version_statement = @fieldParentPtr(Node.VersionStatement, "base", node);
989 try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)});
990 for (version_statement.parts) |part| {
991 try part.dump(tree, writer, indent + 1);
992 }
993 },
994 .block => {
995 const block = @fieldParentPtr(Node.Block, "base", node);
996 try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) });
997 for (block.values) |value| {
998 try value.dump(tree, writer, indent + 1);
999 }
1000 try writer.writeByteNTimes(' ', indent);
1001 try writer.writeAll(block.begin_token.slice(tree.source));
1002 try writer.writeAll("\n");
1003 for (block.children) |child| {
1004 try child.dump(tree, writer, indent + 1);
1005 }
1006 try writer.writeByteNTimes(' ', indent);
1007 try writer.writeAll(block.end_token.slice(tree.source));
1008 try writer.writeAll("\n");
1009 },
1010 .block_value => {
1011 const block_value = @fieldParentPtr(Node.BlockValue, "base", node);
1012 try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) });
1013 for (block_value.values) |value| {
1014 try value.dump(tree, writer, indent + 1);
1015 }
1016 },
1017 .block_value_value => {
1018 const block_value = @fieldParentPtr(Node.BlockValueValue, "base", node);
1019 if (block_value.trailing_comma) {
1020 try writer.writeAll(" ,");
1021 }
1022 try writer.writeAll("\n");
1023 try block_value.expression.dump(tree, writer, indent + 1);
1024 },
1025 .string_table => {
1026 const string_table = @fieldParentPtr(Node.StringTable, "base", node);
1027 try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len });
1028 for (string_table.optional_statements) |statement| {
1029 try statement.dump(tree, writer, indent + 1);
1030 }
1031 try writer.writeByteNTimes(' ', indent);
1032 try writer.writeAll(string_table.begin_token.slice(tree.source));
1033 try writer.writeAll("\n");
1034 for (string_table.strings) |string| {
1035 try string.dump(tree, writer, indent + 1);
1036 }
1037 try writer.writeByteNTimes(' ', indent);
1038 try writer.writeAll(string_table.end_token.slice(tree.source));
1039 try writer.writeAll("\n");
1040 },
1041 .string_table_string => {
1042 try writer.writeAll("\n");
1043 const string = @fieldParentPtr(Node.StringTableString, "base", node);
1044 try string.id.dump(tree, writer, indent + 1);
1045 try writer.writeByteNTimes(' ', indent + 1);
1046 try writer.print("{s}\n", .{string.string.slice(tree.source)});
1047 },
1048 .language_statement => {
1049 const language = @fieldParentPtr(Node.LanguageStatement, "base", node);
1050 try writer.print(" {s}\n", .{language.language_token.slice(tree.source)});
1051 try language.primary_language_id.dump(tree, writer, indent + 1);
1052 try language.sublanguage_id.dump(tree, writer, indent + 1);
1053 },
1054 .font_statement => {
1055 const font = @fieldParentPtr(Node.FontStatement, "base", node);
1056 try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) });
1057 try writer.writeByteNTimes(' ', indent + 1);
1058 try writer.writeAll("point_size:\n");
1059 try font.point_size.dump(tree, writer, indent + 2);
1060 inline for (.{ "weight", "italic", "char_set" }) |arg| {
1061 if (@field(font, arg)) |arg_node| {
1062 try writer.writeByteNTimes(' ', indent + 1);
1063 try writer.writeAll(arg ++ ":\n");
1064 try arg_node.dump(tree, writer, indent + 2);
1065 }
1066 }
1067 },
1068 .simple_statement => {
1069 const statement = @fieldParentPtr(Node.SimpleStatement, "base", node);
1070 try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)});
1071 try statement.value.dump(tree, writer, indent + 1);
1072 },
1073 .invalid => {
1074 const invalid = @fieldParentPtr(Node.Invalid, "base", node);
1075 try writer.print(" context.len: {}\n", .{invalid.context.len});
1076 for (invalid.context) |context_token| {
1077 try writer.writeByteNTimes(' ', indent + 1);
1078 try writer.print("{s}:{s}", .{ @tagName(context_token.id), context_token.slice(tree.source) });
1079 try writer.writeByte('\n');
1080 }
1081 },
1082 }
1083 }
1084};
src/resinator/bmp.zig created+268
......@@ -0,0 +1,268 @@
1//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
2//! https://learn.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)
3//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
4//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
5//! https://archive.org/details/mac_Graphics_File_Formats_Second_Edition_1996/page/n607/mode/2up
6//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
7//!
8//! Notes:
9//! - The Microsoft documentation is incredibly unclear about the color table when the
10//! bit depth is >= 16.
11//! + For bit depth 24 it says "the bmiColors member of BITMAPINFO is NULL" but also
12//! says "the bmiColors color table is used for optimizing colors used on palette-based
13//! devices, and must contain the number of entries specified by the bV5ClrUsed member"
14//! + For bit depth 16 and 32, it seems to imply that if the compression is BI_BITFIELDS
15//! or BI_ALPHABITFIELDS, then the color table *only* consists of the bit masks, but
16//! doesn't really say this outright and the Wikipedia article seems to disagree
17//! For the purposes of this implementation, color tables can always be present for any
18//! bit depth and compression, and the color table follows the header + any optional
19//! bit mask fields dictated by the specified compression.
20
21const std = @import("std");
22const BitmapHeader = @import("ico.zig").BitmapHeader;
23
24pub const windows_format_id = std.mem.readIntNative(u16, "BM");
25pub const file_header_len = 14;
26
27pub const ReadError = error{
28 UnexpectedEOF,
29 InvalidFileHeader,
30 ImpossiblePixelDataOffset,
31 UnknownBitmapVersion,
32 InvalidBitsPerPixel,
33 TooManyColorsInPalette,
34 MissingBitfieldMasks,
35};
36
37pub const BitmapInfo = struct {
38 dib_header_size: u32,
39 /// Contains the interpreted number of colors in the palette (e.g.
40 /// if the field's value is zero and the bit depth is <= 8, this
41 /// will contain the maximum number of colors for the bit depth
42 /// rather than the field's value directly).
43 colors_in_palette: u32,
44 bytes_per_color_palette_element: u8,
45 pixel_data_offset: u32,
46 compression: Compression,
47
48 pub fn getExpectedPaletteByteLen(self: *const BitmapInfo) u64 {
49 return @as(u64, self.colors_in_palette) * self.bytes_per_color_palette_element;
50 }
51
52 pub fn getActualPaletteByteLen(self: *const BitmapInfo) u64 {
53 return self.getByteLenBetweenHeadersAndPixels() - self.getBitmasksByteLen();
54 }
55
56 pub fn getByteLenBetweenHeadersAndPixels(self: *const BitmapInfo) u64 {
57 return @as(u64, self.pixel_data_offset) - self.dib_header_size - file_header_len;
58 }
59
60 pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 {
61 return switch (self.compression) {
62 .BI_BITFIELDS => 12,
63 .BI_ALPHABITFIELDS => 16,
64 else => 0,
65 };
66 }
67
68 pub fn getMissingPaletteByteLen(self: *const BitmapInfo) u64 {
69 if (self.getActualPaletteByteLen() >= self.getExpectedPaletteByteLen()) return 0;
70 return self.getExpectedPaletteByteLen() - self.getActualPaletteByteLen();
71 }
72
73 /// Returns the full byte len of the DIB header + optional bitmasks + color palette
74 pub fn getExpectedByteLenBeforePixelData(self: *const BitmapInfo) u64 {
75 return @as(u64, self.dib_header_size) + self.getBitmasksByteLen() + self.getExpectedPaletteByteLen();
76 }
77
78 /// Returns the full expected byte len
79 pub fn getExpectedByteLen(self: *const BitmapInfo, file_size: u64) u64 {
80 return self.getExpectedByteLenBeforePixelData() + self.getPixelDataLen(file_size);
81 }
82
83 pub fn getPixelDataLen(self: *const BitmapInfo, file_size: u64) u64 {
84 return file_size - self.pixel_data_offset;
85 }
86};
87
88pub fn read(reader: anytype, max_size: u64) ReadError!BitmapInfo {
89 var bitmap_info: BitmapInfo = undefined;
90 const file_header = reader.readBytesNoEof(file_header_len) catch return error.UnexpectedEOF;
91
92 const id = std.mem.readIntNative(u16, file_header[0..2]);
93 if (id != windows_format_id) return error.InvalidFileHeader;
94
95 bitmap_info.pixel_data_offset = std.mem.readIntNative(u32, file_header[10..14]);
96 if (bitmap_info.pixel_data_offset > max_size) return error.ImpossiblePixelDataOffset;
97
98 bitmap_info.dib_header_size = reader.readIntLittle(u32) catch return error.UnexpectedEOF;
99 if (bitmap_info.pixel_data_offset < file_header_len + bitmap_info.dib_header_size) return error.ImpossiblePixelDataOffset;
100 const dib_version = BitmapHeader.Version.get(bitmap_info.dib_header_size);
101 switch (dib_version) {
102 .@"nt3.1", .@"nt4.0", .@"nt5.0" => {
103 var dib_header_buf: [@sizeOf(BITMAPINFOHEADER)]u8 align(@alignOf(BITMAPINFOHEADER)) = undefined;
104 std.mem.writeIntLittle(u32, dib_header_buf[0..4], bitmap_info.dib_header_size);
105 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF;
106 var dib_header: *BITMAPINFOHEADER = @ptrCast(&dib_header_buf);
107 structFieldsLittleToNative(BITMAPINFOHEADER, dib_header);
108
109 bitmap_info.colors_in_palette = try dib_header.numColorsInTable();
110 bitmap_info.bytes_per_color_palette_element = 4;
111 bitmap_info.compression = @enumFromInt(dib_header.biCompression);
112
113 if (bitmap_info.getByteLenBetweenHeadersAndPixels() < bitmap_info.getBitmasksByteLen()) {
114 return error.MissingBitfieldMasks;
115 }
116 },
117 .@"win2.0" => {
118 var dib_header_buf: [@sizeOf(BITMAPCOREHEADER)]u8 align(@alignOf(BITMAPCOREHEADER)) = undefined;
119 std.mem.writeIntLittle(u32, dib_header_buf[0..4], bitmap_info.dib_header_size);
120 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF;
121 var dib_header: *BITMAPCOREHEADER = @ptrCast(&dib_header_buf);
122 structFieldsLittleToNative(BITMAPCOREHEADER, dib_header);
123
124 // > The size of the color palette is calculated from the BitsPerPixel value.
125 // > The color palette has 2, 16, 256, or 0 entries for a BitsPerPixel of
126 // > 1, 4, 8, and 24, respectively.
127 bitmap_info.colors_in_palette = switch (dib_header.bcBitCount) {
128 inline 1, 4, 8 => |bit_count| 1 << bit_count,
129 24 => 0,
130 else => return error.InvalidBitsPerPixel,
131 };
132 bitmap_info.bytes_per_color_palette_element = 3;
133
134 bitmap_info.compression = .BI_RGB;
135 },
136 .unknown => return error.UnknownBitmapVersion,
137 }
138
139 return bitmap_info;
140}
141
142/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
143pub const BITMAPCOREHEADER = extern struct {
144 bcSize: u32,
145 bcWidth: u16,
146 bcHeight: u16,
147 bcPlanes: u16,
148 bcBitCount: u16,
149};
150
151/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
152pub const BITMAPINFOHEADER = extern struct {
153 bcSize: u32,
154 biWidth: i32,
155 biHeight: i32,
156 biPlanes: u16,
157 biBitCount: u16,
158 biCompression: u32,
159 biSizeImage: u32,
160 biXPelsPerMeter: i32,
161 biYPelsPerMeter: i32,
162 biClrUsed: u32,
163 biClrImportant: u32,
164
165 /// Returns error.TooManyColorsInPalette if the number of colors specified
166 /// exceeds the number of possible colors referenced in the pixel data (i.e.
167 /// if 1 bit is used per pixel, then the color table can't have more than 2 colors
168 /// since any more couldn't possibly be indexed in the pixel data)
169 ///
170 /// Returns error.InvalidBitsPerPixel if the bit depth is not 1, 4, 8, 16, 24, or 32.
171 pub fn numColorsInTable(self: BITMAPINFOHEADER) !u32 {
172 switch (self.biBitCount) {
173 inline 1, 4, 8 => |bit_count| switch (self.biClrUsed) {
174 // > If biClrUsed is zero, the array contains the maximum number of
175 // > colors for the given bitdepth; that is, 2^biBitCount colors
176 0 => return 1 << bit_count,
177 // > If biClrUsed is nonzero and the biBitCount member is less than 16,
178 // > the biClrUsed member specifies the actual number of colors the
179 // > graphics engine or device driver accesses.
180 else => {
181 const max_colors = 1 << bit_count;
182 if (self.biClrUsed > max_colors) {
183 return error.TooManyColorsInPalette;
184 }
185 return self.biClrUsed;
186 },
187 },
188 // > If biBitCount is 16 or greater, the biClrUsed member specifies
189 // > the size of the color table used to optimize performance of the
190 // > system color palettes.
191 //
192 // Note: Bit depths >= 16 only use the color table 'for optimizing colors
193 // used on palette-based devices', but it still makes sense to limit their
194 // colors since the pixel data is still limited to this number of colors
195 // (i.e. even though the color table is not indexed by the pixel data,
196 // the color table having more colors than the pixel data can represent
197 // would never make sense and indicates a malformed bitmap).
198 inline 16, 24, 32 => |bit_count| {
199 const max_colors = 1 << bit_count;
200 if (self.biClrUsed > max_colors) {
201 return error.TooManyColorsInPalette;
202 }
203 return self.biClrUsed;
204 },
205 else => return error.InvalidBitsPerPixel,
206 }
207 }
208};
209
210pub const Compression = enum(u32) {
211 BI_RGB = 0,
212 BI_RLE8 = 1,
213 BI_RLE4 = 2,
214 BI_BITFIELDS = 3,
215 BI_JPEG = 4,
216 BI_PNG = 5,
217 BI_ALPHABITFIELDS = 6,
218 BI_CMYK = 11,
219 BI_CMYKRLE8 = 12,
220 BI_CMYKRLE4 = 13,
221 _,
222};
223
224fn structFieldsLittleToNative(comptime T: type, x: *T) void {
225 inline for (@typeInfo(T).Struct.fields) |field| {
226 @field(x, field.name) = std.mem.littleToNative(field.type, @field(x, field.name));
227 }
228}
229
230test "read" {
231 var bmp_data = "BM<\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00\x12\x0b\x00\x00\x12\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x7f\x00\x00\x00\x00".*;
232 var fbs = std.io.fixedBufferStream(&bmp_data);
233
234 {
235 const bitmap = try read(fbs.reader(), bmp_data.len);
236 try std.testing.expectEqual(@as(u32, BitmapHeader.Version.@"nt3.1".len()), bitmap.dib_header_size);
237 }
238
239 {
240 fbs.reset();
241 bmp_data[file_header_len] = 11;
242 try std.testing.expectError(error.UnknownBitmapVersion, read(fbs.reader(), bmp_data.len));
243
244 // restore
245 bmp_data[file_header_len] = BitmapHeader.Version.@"nt3.1".len();
246 }
247
248 {
249 fbs.reset();
250 bmp_data[0] = 'b';
251 try std.testing.expectError(error.InvalidFileHeader, read(fbs.reader(), bmp_data.len));
252
253 // restore
254 bmp_data[0] = 'B';
255 }
256
257 {
258 const cutoff_len = file_header_len + BitmapHeader.Version.@"nt3.1".len() - 1;
259 var dib_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]);
260 try std.testing.expectError(error.UnexpectedEOF, read(dib_cutoff_fbs.reader(), bmp_data.len));
261 }
262
263 {
264 const cutoff_len = file_header_len - 1;
265 var bmp_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]);
266 try std.testing.expectError(error.UnexpectedEOF, read(bmp_cutoff_fbs.reader(), bmp_data.len));
267 }
268}
src/resinator/cli.zig created+1433
......@@ -0,0 +1,1433 @@
1const std = @import("std");
2const CodePage = @import("code_pages.zig").CodePage;
3const lang = @import("lang.zig");
4const res = @import("res.zig");
5const Allocator = std.mem.Allocator;
6const lex = @import("lex.zig");
7
8/// This is what /SL 100 will set the maximum string literal length to
9pub const max_string_literal_length_100_percent = 8192;
10
11pub const usage_string =
12 \\Usage: resinator [options] [--] <INPUT> [<OUTPUT>]
13 \\
14 \\The sequence -- can be used to signify when to stop parsing options.
15 \\This is necessary when the input path begins with a forward slash.
16 \\
17 \\Supported Win32 RC Options:
18 \\ /?, /h Print this help and exit.
19 \\ /v Verbose (print progress messages).
20 \\ /d <name>[=<value>] Define a symbol (during preprocessing).
21 \\ /u <name> Undefine a symbol (during preprocessing).
22 \\ /fo <value> Specify output file path.
23 \\ /l <value> Set default language using hexadecimal id (ex: 409).
24 \\ /ln <value> Set default language using language name (ex: en-us).
25 \\ /i <value> Add an include path.
26 \\ /x Ignore INCLUDE environment variable.
27 \\ /c <value> Set default code page (ex: 65001).
28 \\ /w Warn on invalid code page in .rc (instead of error).
29 \\ /y Suppress warnings for duplicate control IDs.
30 \\ /n Null-terminate all strings in string tables.
31 \\ /sl <value> Specify string literal length limit in percentage (1-100)
32 \\ where 100 corresponds to a limit of 8192. If the /sl
33 \\ option is not specified, the default limit is 4097.
34 \\ /p Only run the preprocessor and output a .rcpp file.
35 \\
36 \\No-op Win32 RC Options:
37 \\ /nologo, /a, /r Options that are recognized but do nothing.
38 \\
39 \\Unsupported Win32 RC Options:
40 \\ /fm, /q, /g, /gn, /g1, /g2 Unsupported MUI-related options.
41 \\ /?c, /hc, /t, /tp:<prefix>, Unsupported LCX/LCE-related options.
42 \\ /tn, /tm, /tc, /tw, /te,
43 \\ /ti, /ta
44 \\ /z Unsupported font-substitution-related option.
45 \\ /s Unsupported HWB-related option.
46 \\
47 \\Custom Options (resinator-specific):
48 \\ /:no-preprocess Do not run the preprocessor.
49 \\ /:debug Output the preprocessed .rc file and the parsed AST.
50 \\ /:auto-includes <value> Set the automatic include path detection behavior.
51 \\ any (default) Use MSVC if available, fall back to MinGW
52 \\ msvc Use MSVC include paths (must be present on the system)
53 \\ gnu Use MinGW include paths (requires Zig as the preprocessor)
54 \\ none Do not use any autodetected include paths
55 \\
56 \\Note: For compatibility reasons, all custom options start with :
57 \\
58;
59
60pub const Diagnostics = struct {
61 errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
62 allocator: Allocator,
63
64 pub const ErrorDetails = struct {
65 arg_index: usize,
66 arg_span: ArgSpan = .{},
67 msg: std.ArrayListUnmanaged(u8) = .{},
68 type: Type = .err,
69 print_args: bool = true,
70
71 pub const Type = enum { err, warning, note };
72 pub const ArgSpan = struct {
73 point_at_next_arg: bool = false,
74 name_offset: usize = 0,
75 prefix_len: usize = 0,
76 value_offset: usize = 0,
77 name_len: usize = 0,
78 };
79 };
80
81 pub fn init(allocator: Allocator) Diagnostics {
82 return .{
83 .allocator = allocator,
84 };
85 }
86
87 pub fn deinit(self: *Diagnostics) void {
88 for (self.errors.items) |*details| {
89 details.msg.deinit(self.allocator);
90 }
91 self.errors.deinit(self.allocator);
92 }
93
94 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
95 try self.errors.append(self.allocator, error_details);
96 }
97
98 pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8, config: std.io.tty.Config) void {
99 std.debug.getStderrMutex().lock();
100 defer std.debug.getStderrMutex().unlock();
101 const stderr = std.io.getStdErr().writer();
102 self.renderToWriter(args, stderr, config) catch return;
103 }
104
105 pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: anytype, config: std.io.tty.Config) !void {
106 for (self.errors.items) |err_details| {
107 try renderErrorMessage(writer, config, err_details, args);
108 }
109 }
110
111 pub fn hasError(self: *const Diagnostics) bool {
112 for (self.errors.items) |err| {
113 if (err.type == .err) return true;
114 }
115 return false;
116 }
117};
118
119pub const Options = struct {
120 allocator: Allocator,
121 input_filename: []const u8 = &[_]u8{},
122 output_filename: []const u8 = &[_]u8{},
123 extra_include_paths: std.ArrayListUnmanaged([]const u8) = .{},
124 ignore_include_env_var: bool = false,
125 preprocess: Preprocess = .yes,
126 default_language_id: ?u16 = null,
127 default_code_page: ?CodePage = null,
128 verbose: bool = false,
129 symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .{},
130 null_terminate_string_table_strings: bool = false,
131 max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
132 silent_duplicate_control_ids: bool = false,
133 warn_instead_of_error_on_invalid_code_page: bool = false,
134 debug: bool = false,
135 print_help_and_exit: bool = false,
136 auto_includes: AutoIncludes = .any,
137
138 pub const AutoIncludes = enum { any, msvc, gnu, none };
139 pub const Preprocess = enum { no, yes, only };
140 pub const SymbolAction = enum { define, undefine };
141 pub const SymbolValue = union(SymbolAction) {
142 define: []const u8,
143 undefine: void,
144
145 pub fn deinit(self: SymbolValue, allocator: Allocator) void {
146 switch (self) {
147 .define => |value| allocator.free(value),
148 .undefine => {},
149 }
150 }
151 };
152
153 /// Does not check that identifier contains only valid characters
154 pub fn define(self: *Options, identifier: []const u8, value: []const u8) !void {
155 if (self.symbols.getPtr(identifier)) |val_ptr| {
156 // If the symbol is undefined, then that always takes precedence so
157 // we shouldn't change anything.
158 if (val_ptr.* == .undefine) return;
159 // Otherwise, the new value takes precedence.
160 var duped_value = try self.allocator.dupe(u8, value);
161 errdefer self.allocator.free(duped_value);
162 val_ptr.deinit(self.allocator);
163 val_ptr.* = .{ .define = duped_value };
164 return;
165 }
166 var duped_key = try self.allocator.dupe(u8, identifier);
167 errdefer self.allocator.free(duped_key);
168 var duped_value = try self.allocator.dupe(u8, value);
169 errdefer self.allocator.free(duped_value);
170 try self.symbols.put(self.allocator, duped_key, .{ .define = duped_value });
171 }
172
173 /// Does not check that identifier contains only valid characters
174 pub fn undefine(self: *Options, identifier: []const u8) !void {
175 if (self.symbols.getPtr(identifier)) |action| {
176 action.deinit(self.allocator);
177 action.* = .{ .undefine = {} };
178 return;
179 }
180 var duped_key = try self.allocator.dupe(u8, identifier);
181 errdefer self.allocator.free(duped_key);
182 try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} });
183 }
184
185 /// If the current input filename both:
186 /// - does not have an extension, and
187 /// - does not exist in the cwd
188 /// then this function will append `.rc` to the input filename
189 ///
190 /// Note: This behavior is different from the Win32 compiler.
191 /// It always appends .RC if the filename does not have
192 /// a `.` in it and it does not even try the verbatim name
193 /// in that scenario.
194 ///
195 /// The approach taken here is meant to give us a 'best of both
196 /// worlds' situation where we'll be compatible with most use-cases
197 /// of the .rc extension being omitted from the CLI args, but still
198 /// work fine if the file itself does not have an extension.
199 pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void {
200 if (std.fs.path.extension(options.input_filename).len == 0) {
201 cwd.access(options.input_filename, .{}) catch |err| switch (err) {
202 error.FileNotFound => {
203 var filename_bytes = try options.allocator.alloc(u8, options.input_filename.len + 3);
204 std.mem.copy(u8, filename_bytes, options.input_filename);
205 std.mem.copy(u8, filename_bytes[filename_bytes.len - 3 ..], ".rc");
206 options.allocator.free(options.input_filename);
207 options.input_filename = filename_bytes;
208 },
209 else => {},
210 };
211 }
212 }
213
214 pub fn deinit(self: *Options) void {
215 for (self.extra_include_paths.items) |extra_include_path| {
216 self.allocator.free(extra_include_path);
217 }
218 self.extra_include_paths.deinit(self.allocator);
219 self.allocator.free(self.input_filename);
220 self.allocator.free(self.output_filename);
221 var symbol_it = self.symbols.iterator();
222 while (symbol_it.next()) |entry| {
223 self.allocator.free(entry.key_ptr.*);
224 entry.value_ptr.deinit(self.allocator);
225 }
226 self.symbols.deinit(self.allocator);
227 }
228
229 pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
230 try writer.print("Input filename: {s}\n", .{self.input_filename});
231 try writer.print("Output filename: {s}\n", .{self.output_filename});
232 if (self.extra_include_paths.items.len > 0) {
233 try writer.writeAll(" Extra include paths:\n");
234 for (self.extra_include_paths.items) |extra_include_path| {
235 try writer.print(" \"{s}\"\n", .{extra_include_path});
236 }
237 }
238 if (self.ignore_include_env_var) {
239 try writer.writeAll(" The INCLUDE environment variable will be ignored\n");
240 }
241 if (self.preprocess == .no) {
242 try writer.writeAll(" The preprocessor will not be invoked\n");
243 } else if (self.preprocess == .only) {
244 try writer.writeAll(" Only the preprocessor will be invoked\n");
245 }
246 if (self.symbols.count() > 0) {
247 try writer.writeAll(" Symbols:\n");
248 var it = self.symbols.iterator();
249 while (it.next()) |symbol| {
250 try writer.print(" {s} {s}", .{ switch (symbol.value_ptr.*) {
251 .define => "#define",
252 .undefine => "#undef",
253 }, symbol.key_ptr.* });
254 if (symbol.value_ptr.* == .define) {
255 try writer.print(" {s}", .{symbol.value_ptr.define});
256 }
257 try writer.writeAll("\n");
258 }
259 }
260 if (self.null_terminate_string_table_strings) {
261 try writer.writeAll(" Strings in string tables will be null-terminated\n");
262 }
263 if (self.max_string_literal_codepoints != lex.default_max_string_literal_codepoints) {
264 try writer.print(" Max string literal length: {}\n", .{self.max_string_literal_codepoints});
265 }
266 if (self.silent_duplicate_control_ids) {
267 try writer.writeAll(" Duplicate control IDs will not emit warnings\n");
268 }
269 if (self.silent_duplicate_control_ids) {
270 try writer.writeAll(" Invalid code page in .rc will produce a warning (instead of an error)\n");
271 }
272
273 const language_id = self.default_language_id orelse res.Language.default;
274 const language_name = language_name: {
275 if (std.meta.intToEnum(lang.LanguageId, language_id)) |lang_enum_val| {
276 break :language_name @tagName(lang_enum_val);
277 } else |_| {}
278 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
279 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
280 }
281 break :language_name "<UNKNOWN>";
282 };
283 try writer.print("Default language: {s} (id=0x{x})\n", .{ language_name, language_id });
284
285 const code_page = self.default_code_page orelse .windows1252;
286 try writer.print("Default codepage: {s} (id={})\n", .{ @tagName(code_page), @intFromEnum(code_page) });
287 }
288};
289
290pub const Arg = struct {
291 prefix: enum { long, short, slash },
292 name_offset: usize,
293 full: []const u8,
294
295 pub fn fromString(str: []const u8) ?@This() {
296 if (std.mem.startsWith(u8, str, "--")) {
297 return .{ .prefix = .long, .name_offset = 2, .full = str };
298 } else if (std.mem.startsWith(u8, str, "-")) {
299 return .{ .prefix = .short, .name_offset = 1, .full = str };
300 } else if (std.mem.startsWith(u8, str, "/")) {
301 return .{ .prefix = .slash, .name_offset = 1, .full = str };
302 }
303 return null;
304 }
305
306 pub fn prefixSlice(self: Arg) []const u8 {
307 return self.full[0..(if (self.prefix == .long) 2 else 1)];
308 }
309
310 pub fn name(self: Arg) []const u8 {
311 return self.full[self.name_offset..];
312 }
313
314 pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 {
315 return self.name()[0..option_len];
316 }
317
318 pub fn missingSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
319 return .{
320 .point_at_next_arg = true,
321 .value_offset = 0,
322 .name_offset = self.name_offset,
323 .prefix_len = self.prefixSlice().len,
324 };
325 }
326
327 pub fn optionAndAfterSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
328 return self.optionSpan(0);
329 }
330
331 pub fn optionSpan(self: Arg, option_len: usize) Diagnostics.ErrorDetails.ArgSpan {
332 return .{
333 .name_offset = self.name_offset,
334 .prefix_len = self.prefixSlice().len,
335 .name_len = option_len,
336 };
337 }
338
339 pub const Value = struct {
340 slice: []const u8,
341 index_increment: u2 = 1,
342
343 pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan {
344 const prefix_len = arg.prefixSlice().len;
345 switch (self.index_increment) {
346 1 => return .{
347 .value_offset = @intFromPtr(self.slice.ptr) - @intFromPtr(arg.full.ptr),
348 .prefix_len = prefix_len,
349 .name_offset = arg.name_offset,
350 },
351 2 => return .{
352 .point_at_next_arg = true,
353 .prefix_len = prefix_len,
354 .name_offset = arg.name_offset,
355 },
356 else => unreachable,
357 }
358 }
359
360 pub fn index(self: Value, arg_index: usize) usize {
361 if (self.index_increment == 2) return arg_index + 1;
362 return arg_index;
363 }
364 };
365
366 pub fn value(self: Arg, option_len: usize, index: usize, args: []const []const u8) error{MissingValue}!Value {
367 const rest = self.full[self.name_offset + option_len ..];
368 if (rest.len > 0) return .{ .slice = rest };
369 if (index + 1 >= args.len) return error.MissingValue;
370 return .{ .slice = args[index + 1], .index_increment = 2 };
371 }
372
373 pub const Context = struct {
374 index: usize,
375 arg: Arg,
376 value: Value,
377 };
378};
379
380pub const ParseError = error{ParseError} || Allocator.Error;
381
382/// Note: Does not run `Options.maybeAppendRC` automatically. If that behavior is desired,
383/// it must be called separately.
384pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options {
385 var options = Options{ .allocator = allocator };
386 errdefer options.deinit();
387
388 var output_filename: ?[]const u8 = null;
389 var output_filename_context: Arg.Context = undefined;
390
391 var arg_i: usize = 1; // start at 1 to skip past the exe name
392 next_arg: while (arg_i < args.len) {
393 var arg = Arg.fromString(args[arg_i]) orelse break;
394 if (arg.name().len == 0) {
395 switch (arg.prefix) {
396 // -- on its own ends arg parsing
397 .long => {
398 arg_i += 1;
399 break;
400 },
401 // - or / on its own is an error
402 else => {
403 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
404 var msg_writer = err_details.msg.writer(allocator);
405 try msg_writer.print("invalid option: {s}", .{arg.prefixSlice()});
406 try diagnostics.append(err_details);
407 arg_i += 1;
408 continue :next_arg;
409 },
410 }
411 }
412
413 while (arg.name().len > 0) {
414 const arg_name = arg.name();
415 // Note: These cases should be in order from longest to shortest, since
416 // shorter options that are a substring of a longer one could make
417 // the longer option's branch unreachable.
418 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
419 options.preprocess = .no;
420 arg.name_offset += ":no-preprocess".len;
421 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
422 const value = arg.value(":auto-includes".len, arg_i, args) catch {
423 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
424 var msg_writer = err_details.msg.writer(allocator);
425 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":auto-includes".len) });
426 try diagnostics.append(err_details);
427 arg_i += 1;
428 break :next_arg;
429 };
430 options.auto_includes = std.meta.stringToEnum(Options.AutoIncludes, value.slice) orelse blk: {
431 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
432 var msg_writer = err_details.msg.writer(allocator);
433 try msg_writer.print("invalid auto includes setting: {s} ", .{value.slice});
434 try diagnostics.append(err_details);
435 break :blk options.auto_includes;
436 };
437 arg_i += value.index_increment;
438 continue :next_arg;
439 } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
440 // No-op, we don't display any 'logo' to suppress
441 arg.name_offset += "nologo".len;
442 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":debug")) {
443 options.debug = true;
444 arg.name_offset += ":debug".len;
445 }
446 // Unsupported LCX/LCE options that need a value (within the same arg only)
447 else if (std.ascii.startsWithIgnoreCase(arg_name, "tp:")) {
448 const rest = arg.full[arg.name_offset + 3 ..];
449 if (rest.len == 0) {
450 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = .{
451 .name_offset = arg.name_offset,
452 .prefix_len = arg.prefixSlice().len,
453 .value_offset = arg.name_offset + 3,
454 } };
455 var msg_writer = err_details.msg.writer(allocator);
456 try msg_writer.print("missing value for {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
457 try diagnostics.append(err_details);
458 }
459 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
460 var msg_writer = err_details.msg.writer(allocator);
461 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
462 try diagnostics.append(err_details);
463 arg_i += 1;
464 continue :next_arg;
465 }
466 // Unsupported LCX/LCE options that need a value
467 else if (std.ascii.startsWithIgnoreCase(arg_name, "tn")) {
468 const value = arg.value(2, arg_i, args) catch no_value: {
469 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
470 var msg_writer = err_details.msg.writer(allocator);
471 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
472 try diagnostics.append(err_details);
473 // dummy zero-length slice starting where the value would have been
474 const value_start = arg.name_offset + 2;
475 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
476 };
477 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
478 var msg_writer = err_details.msg.writer(allocator);
479 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
480 try diagnostics.append(err_details);
481 arg_i += value.index_increment;
482 continue :next_arg;
483 }
484 // Unsupported MUI options that need a value
485 else if (std.ascii.startsWithIgnoreCase(arg_name, "fm") or
486 std.ascii.startsWithIgnoreCase(arg_name, "gn") or
487 std.ascii.startsWithIgnoreCase(arg_name, "g2"))
488 {
489 const value = arg.value(2, arg_i, args) catch no_value: {
490 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
491 var msg_writer = err_details.msg.writer(allocator);
492 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
493 try diagnostics.append(err_details);
494 // dummy zero-length slice starting where the value would have been
495 const value_start = arg.name_offset + 2;
496 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
497 };
498 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
499 var msg_writer = err_details.msg.writer(allocator);
500 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
501 try diagnostics.append(err_details);
502 arg_i += value.index_increment;
503 continue :next_arg;
504 }
505 // Unsupported MUI options that do not need a value
506 else if (std.ascii.startsWithIgnoreCase(arg_name, "g1")) {
507 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
508 var msg_writer = err_details.msg.writer(allocator);
509 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
510 try diagnostics.append(err_details);
511 arg.name_offset += 2;
512 }
513 // Unsupported LCX/LCE options that do not need a value
514 else if (std.ascii.startsWithIgnoreCase(arg_name, "tm") or
515 std.ascii.startsWithIgnoreCase(arg_name, "tc") or
516 std.ascii.startsWithIgnoreCase(arg_name, "tw") or
517 std.ascii.startsWithIgnoreCase(arg_name, "te") or
518 std.ascii.startsWithIgnoreCase(arg_name, "ti") or
519 std.ascii.startsWithIgnoreCase(arg_name, "ta"))
520 {
521 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
522 var msg_writer = err_details.msg.writer(allocator);
523 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
524 try diagnostics.append(err_details);
525 arg.name_offset += 2;
526 } else if (std.ascii.startsWithIgnoreCase(arg_name, "fo")) {
527 const value = arg.value(2, arg_i, args) catch {
528 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
529 var msg_writer = err_details.msg.writer(allocator);
530 try msg_writer.print("missing output path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
531 try diagnostics.append(err_details);
532 arg_i += 1;
533 break :next_arg;
534 };
535 output_filename_context = .{ .index = arg_i, .arg = arg, .value = value };
536 output_filename = value.slice;
537 arg_i += value.index_increment;
538 continue :next_arg;
539 } else if (std.ascii.startsWithIgnoreCase(arg_name, "sl")) {
540 const value = arg.value(2, arg_i, args) catch {
541 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
542 var msg_writer = err_details.msg.writer(allocator);
543 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
544 try diagnostics.append(err_details);
545 arg_i += 1;
546 break :next_arg;
547 };
548 const percent_str = value.slice;
549 const percent: u32 = parsePercent(percent_str) catch {
550 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
551 var msg_writer = err_details.msg.writer(allocator);
552 try msg_writer.print("invalid percent format '{s}'", .{percent_str});
553 try diagnostics.append(err_details);
554 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
555 var note_writer = note_details.msg.writer(allocator);
556 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
557 try diagnostics.append(note_details);
558 arg_i += value.index_increment;
559 continue :next_arg;
560 };
561 if (percent == 0 or percent > 100) {
562 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
563 var msg_writer = err_details.msg.writer(allocator);
564 try msg_writer.print("percent out of range: {} (parsed from '{s}')", .{ percent, percent_str });
565 try diagnostics.append(err_details);
566 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
567 var note_writer = note_details.msg.writer(allocator);
568 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
569 try diagnostics.append(note_details);
570 arg_i += value.index_increment;
571 continue :next_arg;
572 }
573 const percent_float = @as(f32, @floatFromInt(percent)) / 100;
574 options.max_string_literal_codepoints = @intFromFloat(percent_float * max_string_literal_length_100_percent);
575 arg_i += value.index_increment;
576 continue :next_arg;
577 } else if (std.ascii.startsWithIgnoreCase(arg_name, "ln")) {
578 const value = arg.value(2, arg_i, args) catch {
579 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
580 var msg_writer = err_details.msg.writer(allocator);
581 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
582 try diagnostics.append(err_details);
583 arg_i += 1;
584 break :next_arg;
585 };
586 const tag = value.slice;
587 options.default_language_id = lang.tagToInt(tag) catch {
588 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
589 var msg_writer = err_details.msg.writer(allocator);
590 try msg_writer.print("invalid language tag: {s}", .{tag});
591 try diagnostics.append(err_details);
592 arg_i += value.index_increment;
593 continue :next_arg;
594 };
595 if (options.default_language_id.? == lang.LOCALE_CUSTOM_UNSPECIFIED) {
596 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
597 var msg_writer = err_details.msg.writer(allocator);
598 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 });
599 try diagnostics.append(err_details);
600 }
601 arg_i += value.index_increment;
602 continue :next_arg;
603 } else if (std.ascii.startsWithIgnoreCase(arg_name, "l")) {
604 const value = arg.value(1, arg_i, args) catch {
605 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
606 var msg_writer = err_details.msg.writer(allocator);
607 try msg_writer.print("missing language ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
608 try diagnostics.append(err_details);
609 arg_i += 1;
610 break :next_arg;
611 };
612 const num_str = value.slice;
613 options.default_language_id = lang.parseInt(num_str) catch {
614 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
615 var msg_writer = err_details.msg.writer(allocator);
616 try msg_writer.print("invalid language ID: {s}", .{num_str});
617 try diagnostics.append(err_details);
618 arg_i += value.index_increment;
619 continue :next_arg;
620 };
621 arg_i += value.index_increment;
622 continue :next_arg;
623 } else if (std.ascii.startsWithIgnoreCase(arg_name, "h") or std.mem.startsWith(u8, arg_name, "?")) {
624 options.print_help_and_exit = true;
625 // If there's been an error to this point, then we still want to fail
626 if (diagnostics.hasError()) return error.ParseError;
627 return options;
628 }
629 // 1 char unsupported MUI options that need a value
630 else if (std.ascii.startsWithIgnoreCase(arg_name, "q") or
631 std.ascii.startsWithIgnoreCase(arg_name, "g"))
632 {
633 const value = arg.value(1, arg_i, args) catch no_value: {
634 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
635 var msg_writer = err_details.msg.writer(allocator);
636 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
637 try diagnostics.append(err_details);
638 // dummy zero-length slice starting where the value would have been
639 const value_start = arg.name_offset + 1;
640 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
641 };
642 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
643 var msg_writer = err_details.msg.writer(allocator);
644 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
645 try diagnostics.append(err_details);
646 arg_i += value.index_increment;
647 continue :next_arg;
648 }
649 // Undocumented (and unsupported) options that need a value
650 // /z has to do something with font substitution
651 // /s has something to do with HWB resources being inserted into the .res
652 else if (std.ascii.startsWithIgnoreCase(arg_name, "z") or
653 std.ascii.startsWithIgnoreCase(arg_name, "s"))
654 {
655 const value = arg.value(1, arg_i, args) catch no_value: {
656 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
657 var msg_writer = err_details.msg.writer(allocator);
658 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
659 try diagnostics.append(err_details);
660 // dummy zero-length slice starting where the value would have been
661 const value_start = arg.name_offset + 1;
662 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
663 };
664 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
665 var msg_writer = err_details.msg.writer(allocator);
666 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
667 try diagnostics.append(err_details);
668 arg_i += value.index_increment;
669 continue :next_arg;
670 }
671 // 1 char unsupported LCX/LCE options that do not need a value
672 else if (std.ascii.startsWithIgnoreCase(arg_name, "t")) {
673 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
674 var msg_writer = err_details.msg.writer(allocator);
675 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
676 try diagnostics.append(err_details);
677 arg.name_offset += 1;
678 } else if (std.ascii.startsWithIgnoreCase(arg_name, "c")) {
679 const value = arg.value(1, arg_i, args) catch {
680 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
681 var msg_writer = err_details.msg.writer(allocator);
682 try msg_writer.print("missing code page ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
683 try diagnostics.append(err_details);
684 arg_i += 1;
685 break :next_arg;
686 };
687 const num_str = value.slice;
688 const code_page_id = std.fmt.parseUnsigned(u16, num_str, 10) catch {
689 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
690 var msg_writer = err_details.msg.writer(allocator);
691 try msg_writer.print("invalid code page ID: {s}", .{num_str});
692 try diagnostics.append(err_details);
693 arg_i += value.index_increment;
694 continue :next_arg;
695 };
696 options.default_code_page = CodePage.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {
697 error.InvalidCodePage => {
698 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
699 var msg_writer = err_details.msg.writer(allocator);
700 try msg_writer.print("invalid or unknown code page ID: {}", .{code_page_id});
701 try diagnostics.append(err_details);
702 arg_i += value.index_increment;
703 continue :next_arg;
704 },
705 error.UnsupportedCodePage => {
706 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
707 var msg_writer = err_details.msg.writer(allocator);
708 try msg_writer.print("unsupported code page: {s} (id={})", .{
709 @tagName(CodePage.getByIdentifier(code_page_id) catch unreachable),
710 code_page_id,
711 });
712 try diagnostics.append(err_details);
713 arg_i += value.index_increment;
714 continue :next_arg;
715 },
716 };
717 arg_i += value.index_increment;
718 continue :next_arg;
719 } else if (std.ascii.startsWithIgnoreCase(arg_name, "v")) {
720 options.verbose = true;
721 arg.name_offset += 1;
722 } else if (std.ascii.startsWithIgnoreCase(arg_name, "x")) {
723 options.ignore_include_env_var = true;
724 arg.name_offset += 1;
725 } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) {
726 options.preprocess = .only;
727 arg.name_offset += 1;
728 } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) {
729 const value = arg.value(1, arg_i, args) catch {
730 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
731 var msg_writer = err_details.msg.writer(allocator);
732 try msg_writer.print("missing include path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
733 try diagnostics.append(err_details);
734 arg_i += 1;
735 break :next_arg;
736 };
737 const path = value.slice;
738 const duped = try allocator.dupe(u8, path);
739 errdefer allocator.free(duped);
740 try options.extra_include_paths.append(options.allocator, duped);
741 arg_i += value.index_increment;
742 continue :next_arg;
743 } else if (std.ascii.startsWithIgnoreCase(arg_name, "r")) {
744 // From https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
745 // "Ignored. Provided for compatibility with existing makefiles."
746 arg.name_offset += 1;
747 } else if (std.ascii.startsWithIgnoreCase(arg_name, "n")) {
748 options.null_terminate_string_table_strings = true;
749 arg.name_offset += 1;
750 } else if (std.ascii.startsWithIgnoreCase(arg_name, "y")) {
751 options.silent_duplicate_control_ids = true;
752 arg.name_offset += 1;
753 } else if (std.ascii.startsWithIgnoreCase(arg_name, "w")) {
754 options.warn_instead_of_error_on_invalid_code_page = true;
755 arg.name_offset += 1;
756 } else if (std.ascii.startsWithIgnoreCase(arg_name, "a")) {
757 // Undocumented option with unknown function
758 // TODO: More investigation to figure out what it does (if anything)
759 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
760 var msg_writer = err_details.msg.writer(allocator);
761 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) });
762 try diagnostics.append(err_details);
763 arg.name_offset += 1;
764 } else if (std.ascii.startsWithIgnoreCase(arg_name, "d")) {
765 const value = arg.value(1, arg_i, args) catch {
766 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
767 var msg_writer = err_details.msg.writer(allocator);
768 try msg_writer.print("missing symbol to define after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
769 try diagnostics.append(err_details);
770 arg_i += 1;
771 break :next_arg;
772 };
773 var tokenizer = std.mem.tokenize(u8, value.slice, "=");
774 // guaranteed to exist since an empty value.slice would invoke
775 // the 'missing symbol to define' branch above
776 const symbol = tokenizer.next().?;
777 const symbol_value = tokenizer.next() orelse "1";
778
779 if (isValidIdentifier(symbol)) {
780 try options.define(symbol, symbol_value);
781 } else {
782 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
783 var msg_writer = err_details.msg.writer(allocator);
784 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be defined", .{symbol});
785 try diagnostics.append(err_details);
786 }
787 arg_i += value.index_increment;
788 continue :next_arg;
789 } else if (std.ascii.startsWithIgnoreCase(arg_name, "u")) {
790 const value = arg.value(1, arg_i, args) catch {
791 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
792 var msg_writer = err_details.msg.writer(allocator);
793 try msg_writer.print("missing symbol to undefine after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
794 try diagnostics.append(err_details);
795 arg_i += 1;
796 break :next_arg;
797 };
798 const symbol = value.slice;
799 if (isValidIdentifier(symbol)) {
800 try options.undefine(symbol);
801 } else {
802 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
803 var msg_writer = err_details.msg.writer(allocator);
804 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be undefined", .{symbol});
805 try diagnostics.append(err_details);
806 }
807 arg_i += value.index_increment;
808 continue :next_arg;
809 } else {
810 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
811 var msg_writer = err_details.msg.writer(allocator);
812 try msg_writer.print("invalid option: {s}{s}", .{ arg.prefixSlice(), arg.name() });
813 try diagnostics.append(err_details);
814 arg_i += 1;
815 continue :next_arg;
816 }
817 } else {
818 // The while loop exited via its conditional, meaning we are done with
819 // the current arg and can move on the the next
820 arg_i += 1;
821 continue;
822 }
823 }
824
825 var positionals = args[arg_i..];
826
827 if (positionals.len < 1) {
828 var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };
829 var msg_writer = err_details.msg.writer(allocator);
830 try msg_writer.writeAll("missing input filename");
831 try diagnostics.append(err_details);
832
833 const last_arg = args[args.len - 1];
834 if (arg_i > 1 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {
835 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
836 var note_writer = note_details.msg.writer(allocator);
837 try note_writer.writeAll("if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing");
838 try diagnostics.append(note_details);
839 }
840
841 // This is a fatal enough problem to justify an early return, since
842 // things after this rely on the value of the input filename.
843 return error.ParseError;
844 }
845 options.input_filename = try allocator.dupe(u8, positionals[0]);
846
847 if (positionals.len > 1) {
848 if (output_filename != null) {
849 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i + 1 };
850 var msg_writer = err_details.msg.writer(allocator);
851 try msg_writer.writeAll("output filename already specified");
852 try diagnostics.append(err_details);
853 var note_details = Diagnostics.ErrorDetails{
854 .type = .note,
855 .arg_index = output_filename_context.value.index(output_filename_context.index),
856 .arg_span = output_filename_context.value.argSpan(output_filename_context.arg),
857 };
858 var note_writer = note_details.msg.writer(allocator);
859 try note_writer.writeAll("output filename previously specified here");
860 try diagnostics.append(note_details);
861 } else {
862 output_filename = positionals[1];
863 }
864 }
865 if (output_filename == null) {
866 var buf = std.ArrayList(u8).init(allocator);
867 errdefer buf.deinit();
868
869 if (std.fs.path.dirname(options.input_filename)) |dirname| {
870 var end_pos = dirname.len;
871 // We want to ensure that we write a path separator at the end, so if the dirname
872 // doesn't end with a path sep then include the char after the dirname
873 // which must be a path sep.
874 if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1;
875 try buf.appendSlice(options.input_filename[0..end_pos]);
876 }
877 try buf.appendSlice(std.fs.path.stem(options.input_filename));
878 if (options.preprocess == .only) {
879 try buf.appendSlice(".rcpp");
880 } else {
881 try buf.appendSlice(".res");
882 }
883
884 options.output_filename = try buf.toOwnedSlice();
885 } else {
886 options.output_filename = try allocator.dupe(u8, output_filename.?);
887 }
888
889 if (diagnostics.hasError()) {
890 return error.ParseError;
891 }
892
893 return options;
894}
895
896/// Returns true if the str is a valid C identifier for use in a #define/#undef macro
897pub fn isValidIdentifier(str: []const u8) bool {
898 for (str, 0..) |c, i| switch (c) {
899 '0'...'9' => if (i == 0) return false,
900 'a'...'z', 'A'...'Z', '_' => {},
901 else => return false,
902 };
903 return true;
904}
905
906/// This function is specific to how the Win32 RC command line interprets
907/// max string literal length percent.
908/// - Wraps on overflow of u32
909/// - Stops parsing on any invalid hexadecimal digits
910/// - Errors if a digit is not the first char
911/// - `-` (negative) prefix is allowed
912pub fn parsePercent(str: []const u8) error{InvalidFormat}!u32 {
913 var result: u32 = 0;
914 const radix: u8 = 10;
915 var buf = str;
916
917 const Prefix = enum { none, minus };
918 var prefix: Prefix = .none;
919 switch (buf[0]) {
920 '-' => {
921 prefix = .minus;
922 buf = buf[1..];
923 },
924 else => {},
925 }
926
927 for (buf, 0..) |c, i| {
928 const digit = switch (c) {
929 // On invalid digit for the radix, just stop parsing but don't fail
930 '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
931 else => {
932 // First digit must be valid
933 if (i == 0) {
934 return error.InvalidFormat;
935 }
936 break;
937 },
938 };
939
940 if (result != 0) {
941 result *%= radix;
942 }
943 result +%= digit;
944 }
945
946 switch (prefix) {
947 .none => {},
948 .minus => result = 0 -% result,
949 }
950
951 return result;
952}
953
954test parsePercent {
955 try std.testing.expectEqual(@as(u32, 16), try parsePercent("16"));
956 try std.testing.expectEqual(@as(u32, 0), try parsePercent("0x1A"));
957 try std.testing.expectEqual(@as(u32, 0x1), try parsePercent("1zzzz"));
958 try std.testing.expectEqual(@as(u32, 0xffffffff), try parsePercent("-1"));
959 try std.testing.expectEqual(@as(u32, 0xfffffff0), try parsePercent("-16"));
960 try std.testing.expectEqual(@as(u32, 1), try parsePercent("4294967297"));
961 try std.testing.expectError(error.InvalidFormat, parsePercent("--1"));
962 try std.testing.expectError(error.InvalidFormat, parsePercent("ha"));
963 try std.testing.expectError(error.InvalidFormat, parsePercent("¹"));
964 try std.testing.expectError(error.InvalidFormat, parsePercent("~1"));
965}
966
967pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, err_details: Diagnostics.ErrorDetails, args: []const []const u8) !void {
968 try config.setColor(writer, .dim);
969 try writer.writeAll("<cli>");
970 try config.setColor(writer, .reset);
971 try config.setColor(writer, .bold);
972 try writer.writeAll(": ");
973 switch (err_details.type) {
974 .err => {
975 try config.setColor(writer, .red);
976 try writer.writeAll("error: ");
977 },
978 .warning => {
979 try config.setColor(writer, .yellow);
980 try writer.writeAll("warning: ");
981 },
982 .note => {
983 try config.setColor(writer, .cyan);
984 try writer.writeAll("note: ");
985 },
986 }
987 try config.setColor(writer, .reset);
988 try config.setColor(writer, .bold);
989 try writer.writeAll(err_details.msg.items);
990 try writer.writeByte('\n');
991 try config.setColor(writer, .reset);
992
993 if (!err_details.print_args) {
994 try writer.writeByte('\n');
995 return;
996 }
997
998 try config.setColor(writer, .dim);
999 const prefix = " ... ";
1000 try writer.writeAll(prefix);
1001 try config.setColor(writer, .reset);
1002
1003 const arg_with_name = args[err_details.arg_index];
1004 const prefix_slice = arg_with_name[0..err_details.arg_span.prefix_len];
1005 const before_name_slice = arg_with_name[err_details.arg_span.prefix_len..err_details.arg_span.name_offset];
1006 var name_slice = arg_with_name[err_details.arg_span.name_offset..];
1007 if (err_details.arg_span.name_len > 0) name_slice.len = err_details.arg_span.name_len;
1008 const after_name_slice = arg_with_name[err_details.arg_span.name_offset + name_slice.len ..];
1009
1010 try writer.writeAll(prefix_slice);
1011 if (before_name_slice.len > 0) {
1012 try config.setColor(writer, .dim);
1013 try writer.writeAll(before_name_slice);
1014 try config.setColor(writer, .reset);
1015 }
1016 try writer.writeAll(name_slice);
1017 if (after_name_slice.len > 0) {
1018 try config.setColor(writer, .dim);
1019 try writer.writeAll(after_name_slice);
1020 try config.setColor(writer, .reset);
1021 }
1022
1023 var next_arg_len: usize = 0;
1024 if (err_details.arg_span.point_at_next_arg and err_details.arg_index + 1 < args.len) {
1025 const next_arg = args[err_details.arg_index + 1];
1026 try writer.writeByte(' ');
1027 try writer.writeAll(next_arg);
1028 next_arg_len = next_arg.len;
1029 }
1030
1031 const last_shown_arg_index = if (err_details.arg_span.point_at_next_arg) err_details.arg_index + 1 else err_details.arg_index;
1032 if (last_shown_arg_index + 1 < args.len) {
1033 // special case for when pointing to a missing value within the same arg
1034 // as the name
1035 if (err_details.arg_span.value_offset >= arg_with_name.len) {
1036 try writer.writeByte(' ');
1037 }
1038 try config.setColor(writer, .dim);
1039 try writer.writeAll(" ...");
1040 try config.setColor(writer, .reset);
1041 }
1042 try writer.writeByte('\n');
1043
1044 try config.setColor(writer, .green);
1045 try writer.writeByteNTimes(' ', prefix.len);
1046 // Special case for when the option is *only* a prefix (e.g. invalid option: -)
1047 if (err_details.arg_span.prefix_len == arg_with_name.len) {
1048 try writer.writeByteNTimes('^', err_details.arg_span.prefix_len);
1049 } else {
1050 try writer.writeByteNTimes('~', err_details.arg_span.prefix_len);
1051 try writer.writeByteNTimes(' ', err_details.arg_span.name_offset - err_details.arg_span.prefix_len);
1052 if (!err_details.arg_span.point_at_next_arg and err_details.arg_span.value_offset == 0) {
1053 try writer.writeByte('^');
1054 try writer.writeByteNTimes('~', name_slice.len - 1);
1055 } else if (err_details.arg_span.value_offset > 0) {
1056 try writer.writeByteNTimes('~', err_details.arg_span.value_offset - err_details.arg_span.name_offset);
1057 try writer.writeByte('^');
1058 if (err_details.arg_span.value_offset < arg_with_name.len) {
1059 try writer.writeByteNTimes('~', arg_with_name.len - err_details.arg_span.value_offset - 1);
1060 }
1061 } else if (err_details.arg_span.point_at_next_arg) {
1062 try writer.writeByteNTimes('~', arg_with_name.len - err_details.arg_span.name_offset + 1);
1063 try writer.writeByte('^');
1064 if (next_arg_len > 0) {
1065 try writer.writeByteNTimes('~', next_arg_len - 1);
1066 }
1067 }
1068 }
1069 try writer.writeByte('\n');
1070 try config.setColor(writer, .reset);
1071}
1072
1073fn testParse(args: []const []const u8) !Options {
1074 return (try testParseOutput(args, "")).?;
1075}
1076
1077fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options {
1078 return (try testParseOutput(args, expected_output)).?;
1079}
1080
1081fn testParseError(args: []const []const u8, expected_output: []const u8) !void {
1082 var maybe_options = try testParseOutput(args, expected_output);
1083 if (maybe_options != null) {
1084 std.debug.print("expected error, got options: {}\n", .{maybe_options.?});
1085 maybe_options.?.deinit();
1086 return error.TestExpectedError;
1087 }
1088}
1089
1090fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Options {
1091 var diagnostics = Diagnostics.init(std.testing.allocator);
1092 defer diagnostics.deinit();
1093
1094 var output = std.ArrayList(u8).init(std.testing.allocator);
1095 defer output.deinit();
1096
1097 var options = parse(std.testing.allocator, args, &diagnostics) catch |err| switch (err) {
1098 error.ParseError => {
1099 try diagnostics.renderToWriter(args, output.writer(), .no_color);
1100 try std.testing.expectEqualStrings(expected_output, output.items);
1101 return null;
1102 },
1103 else => |e| return e,
1104 };
1105 errdefer options.deinit();
1106
1107 try diagnostics.renderToWriter(args, output.writer(), .no_color);
1108 try std.testing.expectEqualStrings(expected_output, output.items);
1109 return options;
1110}
1111
1112test "parse errors: basic" {
1113 try testParseError(&.{ "foo.exe", "/" },
1114 \\<cli>: error: invalid option: /
1115 \\ ... /
1116 \\ ^
1117 \\<cli>: error: missing input filename
1118 \\
1119 \\
1120 );
1121 try testParseError(&.{ "foo.exe", "/ln" },
1122 \\<cli>: error: missing language tag after /ln option
1123 \\ ... /ln
1124 \\ ~~~~^
1125 \\<cli>: error: missing input filename
1126 \\
1127 \\
1128 );
1129 try testParseError(&.{ "foo.exe", "-vln" },
1130 \\<cli>: error: missing language tag after -ln option
1131 \\ ... -vln
1132 \\ ~ ~~~^
1133 \\<cli>: error: missing input filename
1134 \\
1135 \\
1136 );
1137 try testParseError(&.{ "foo.exe", "/_not-an-option" },
1138 \\<cli>: error: invalid option: /_not-an-option
1139 \\ ... /_not-an-option
1140 \\ ~^~~~~~~~~~~~~~
1141 \\<cli>: error: missing input filename
1142 \\
1143 \\
1144 );
1145 try testParseError(&.{ "foo.exe", "-_not-an-option" },
1146 \\<cli>: error: invalid option: -_not-an-option
1147 \\ ... -_not-an-option
1148 \\ ~^~~~~~~~~~~~~~
1149 \\<cli>: error: missing input filename
1150 \\
1151 \\
1152 );
1153 try testParseError(&.{ "foo.exe", "--_not-an-option" },
1154 \\<cli>: error: invalid option: --_not-an-option
1155 \\ ... --_not-an-option
1156 \\ ~~^~~~~~~~~~~~~~
1157 \\<cli>: error: missing input filename
1158 \\
1159 \\
1160 );
1161 try testParseError(&.{ "foo.exe", "/v_not-an-option" },
1162 \\<cli>: error: invalid option: /_not-an-option
1163 \\ ... /v_not-an-option
1164 \\ ~ ^~~~~~~~~~~~~~
1165 \\<cli>: error: missing input filename
1166 \\
1167 \\
1168 );
1169 try testParseError(&.{ "foo.exe", "-v_not-an-option" },
1170 \\<cli>: error: invalid option: -_not-an-option
1171 \\ ... -v_not-an-option
1172 \\ ~ ^~~~~~~~~~~~~~
1173 \\<cli>: error: missing input filename
1174 \\
1175 \\
1176 );
1177 try testParseError(&.{ "foo.exe", "--v_not-an-option" },
1178 \\<cli>: error: invalid option: --_not-an-option
1179 \\ ... --v_not-an-option
1180 \\ ~~ ^~~~~~~~~~~~~~
1181 \\<cli>: error: missing input filename
1182 \\
1183 \\
1184 );
1185 try testParseError(&.{ "foo.exe", "/some/absolute/path/parsed/as/an/option.rc" },
1186 \\<cli>: error: the /s option is unsupported
1187 \\ ... /some/absolute/path/parsed/as/an/option.rc
1188 \\ ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1189 \\<cli>: error: missing input filename
1190 \\
1191 \\<cli>: note: if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing
1192 \\ ... /some/absolute/path/parsed/as/an/option.rc
1193 \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1194 \\
1195 );
1196}
1197
1198test "parse errors: /ln" {
1199 try testParseError(&.{ "foo.exe", "/ln", "invalid", "foo.rc" },
1200 \\<cli>: error: invalid language tag: invalid
1201 \\ ... /ln invalid ...
1202 \\ ~~~~^~~~~~~
1203 \\
1204 );
1205 try testParseError(&.{ "foo.exe", "/lninvalid", "foo.rc" },
1206 \\<cli>: error: invalid language tag: invalid
1207 \\ ... /lninvalid ...
1208 \\ ~~~^~~~~~~
1209 \\
1210 );
1211}
1212
1213test "parse: options" {
1214 {
1215 var options = try testParse(&.{ "foo.exe", "/v", "foo.rc" });
1216 defer options.deinit();
1217
1218 try std.testing.expectEqual(true, options.verbose);
1219 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1220 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1221 }
1222 {
1223 var options = try testParse(&.{ "foo.exe", "/vx", "foo.rc" });
1224 defer options.deinit();
1225
1226 try std.testing.expectEqual(true, options.verbose);
1227 try std.testing.expectEqual(true, options.ignore_include_env_var);
1228 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1229 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1230 }
1231 {
1232 var options = try testParse(&.{ "foo.exe", "/xv", "foo.rc" });
1233 defer options.deinit();
1234
1235 try std.testing.expectEqual(true, options.verbose);
1236 try std.testing.expectEqual(true, options.ignore_include_env_var);
1237 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1238 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1239 }
1240 {
1241 var options = try testParse(&.{ "foo.exe", "/xvFObar.res", "foo.rc" });
1242 defer options.deinit();
1243
1244 try std.testing.expectEqual(true, options.verbose);
1245 try std.testing.expectEqual(true, options.ignore_include_env_var);
1246 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1247 try std.testing.expectEqualStrings("bar.res", options.output_filename);
1248 }
1249}
1250
1251test "parse: define and undefine" {
1252 {
1253 var options = try testParse(&.{ "foo.exe", "/dfoo", "foo.rc" });
1254 defer options.deinit();
1255
1256 const action = options.symbols.get("foo").?;
1257 try std.testing.expectEqual(Options.SymbolAction.define, action);
1258 try std.testing.expectEqualStrings("1", action.define);
1259 }
1260 {
1261 var options = try testParse(&.{ "foo.exe", "/dfoo=bar", "/dfoo=baz", "foo.rc" });
1262 defer options.deinit();
1263
1264 const action = options.symbols.get("foo").?;
1265 try std.testing.expectEqual(Options.SymbolAction.define, action);
1266 try std.testing.expectEqualStrings("baz", action.define);
1267 }
1268 {
1269 var options = try testParse(&.{ "foo.exe", "/ufoo", "foo.rc" });
1270 defer options.deinit();
1271
1272 const action = options.symbols.get("foo").?;
1273 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1274 }
1275 {
1276 // Once undefined, future defines are ignored
1277 var options = try testParse(&.{ "foo.exe", "/ufoo", "/dfoo", "foo.rc" });
1278 defer options.deinit();
1279
1280 const action = options.symbols.get("foo").?;
1281 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1282 }
1283 {
1284 // Undefined always takes precedence
1285 var options = try testParse(&.{ "foo.exe", "/dfoo", "/ufoo", "/dfoo", "foo.rc" });
1286 defer options.deinit();
1287
1288 const action = options.symbols.get("foo").?;
1289 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1290 }
1291 {
1292 // Warn + ignore invalid identifiers
1293 var options = try testParseWarning(
1294 &.{ "foo.exe", "/dfoo bar", "/u", "0leadingdigit", "foo.rc" },
1295 \\<cli>: warning: symbol "foo bar" is not a valid identifier and therefore cannot be defined
1296 \\ ... /dfoo bar ...
1297 \\ ~~^~~~~~~
1298 \\<cli>: warning: symbol "0leadingdigit" is not a valid identifier and therefore cannot be undefined
1299 \\ ... /u 0leadingdigit ...
1300 \\ ~~~^~~~~~~~~~~~~
1301 \\
1302 ,
1303 );
1304 defer options.deinit();
1305
1306 try std.testing.expectEqual(@as(usize, 0), options.symbols.count());
1307 }
1308}
1309
1310test "parse: /sl" {
1311 try testParseError(&.{ "foo.exe", "/sl", "0", "foo.rc" },
1312 \\<cli>: error: percent out of range: 0 (parsed from '0')
1313 \\ ... /sl 0 ...
1314 \\ ~~~~^
1315 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1316 \\
1317 \\
1318 );
1319 try testParseError(&.{ "foo.exe", "/sl", "abcd", "foo.rc" },
1320 \\<cli>: error: invalid percent format 'abcd'
1321 \\ ... /sl abcd ...
1322 \\ ~~~~^~~~
1323 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1324 \\
1325 \\
1326 );
1327 {
1328 var options = try testParse(&.{ "foo.exe", "foo.rc" });
1329 defer options.deinit();
1330
1331 try std.testing.expectEqual(@as(u15, lex.default_max_string_literal_codepoints), options.max_string_literal_codepoints);
1332 }
1333 {
1334 var options = try testParse(&.{ "foo.exe", "/sl100", "foo.rc" });
1335 defer options.deinit();
1336
1337 try std.testing.expectEqual(@as(u15, max_string_literal_length_100_percent), options.max_string_literal_codepoints);
1338 }
1339 {
1340 var options = try testParse(&.{ "foo.exe", "-SL33", "foo.rc" });
1341 defer options.deinit();
1342
1343 try std.testing.expectEqual(@as(u15, 2703), options.max_string_literal_codepoints);
1344 }
1345 {
1346 var options = try testParse(&.{ "foo.exe", "/sl15", "foo.rc" });
1347 defer options.deinit();
1348
1349 try std.testing.expectEqual(@as(u15, 1228), options.max_string_literal_codepoints);
1350 }
1351}
1352
1353test "parse: unsupported MUI-related options" {
1354 try testParseError(&.{ "foo.exe", "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" },
1355 \\<cli>: error: the /q option is unsupported
1356 \\ ... /q ...
1357 \\ ~^
1358 \\<cli>: error: the /g1 option is unsupported
1359 \\ ... /g1 ...
1360 \\ ~^~
1361 \\<cli>: error: the -G2 option is unsupported
1362 \\ ... -G2 ...
1363 \\ ~^~
1364 \\<cli>: error: the /fm option is unsupported
1365 \\ ... /fm ...
1366 \\ ~^~
1367 \\<cli>: error: the /g option is unsupported
1368 \\ ... /g ...
1369 \\ ~^
1370 \\
1371 );
1372}
1373
1374test "parse: unsupported LCX/LCE-related options" {
1375 try testParseError(&.{ "foo.exe", "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" },
1376 \\<cli>: error: the /t option is unsupported
1377 \\ ... /t ...
1378 \\ ~^
1379 \\<cli>: error: missing value for /tp: option
1380 \\ ... /tp: ...
1381 \\ ~~~~^
1382 \\<cli>: error: the /tp: option is unsupported
1383 \\ ... /tp: ...
1384 \\ ~^~~
1385 \\<cli>: error: the /tp: option is unsupported
1386 \\ ... /tp:blah ...
1387 \\ ~^~~~~~~
1388 \\<cli>: error: the /tm option is unsupported
1389 \\ ... /tm ...
1390 \\ ~^~
1391 \\<cli>: error: the /tc option is unsupported
1392 \\ ... /tc ...
1393 \\ ~^~
1394 \\<cli>: error: the /tw option is unsupported
1395 \\ ... /tw ...
1396 \\ ~^~
1397 \\<cli>: error: the -TE option is unsupported
1398 \\ ... -TEti ...
1399 \\ ~^~
1400 \\<cli>: error: the -ti option is unsupported
1401 \\ ... -TEti ...
1402 \\ ~ ^~
1403 \\<cli>: error: the /ta option is unsupported
1404 \\ ... /ta ...
1405 \\ ~^~
1406 \\<cli>: error: the /tn option is unsupported
1407 \\ ... /tn ...
1408 \\ ~^~
1409 \\
1410 );
1411}
1412
1413test "maybeAppendRC" {
1414 var tmp = std.testing.tmpDir(.{});
1415 defer tmp.cleanup();
1416
1417 var options = try testParse(&.{ "foo.exe", "foo" });
1418 defer options.deinit();
1419 try std.testing.expectEqualStrings("foo", options.input_filename);
1420
1421 // Create the file so that it's found. In this scenario, .rc should not get
1422 // appended.
1423 var file = try tmp.dir.createFile("foo", .{});
1424 file.close();
1425 try options.maybeAppendRC(tmp.dir);
1426 try std.testing.expectEqualStrings("foo", options.input_filename);
1427
1428 // Now delete the file and try again. Since the verbatim name is no longer found
1429 // and the input filename does not have an extension, .rc should get appended.
1430 try tmp.dir.deleteFile("foo");
1431 try options.maybeAppendRC(tmp.dir);
1432 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1433}
src/resinator/code_pages.zig created+487
......@@ -0,0 +1,487 @@
1const std = @import("std");
2const windows1252 = @import("windows1252.zig");
3
4// TODO: Parts of this comment block may be more relevant to string/NameOrOrdinal parsing
5// than it is to the stuff in this file.
6//
7// ‰ representations for context:
8// Win-1252 89
9// UTF-8 E2 80 B0
10// UTF-16 20 30
11//
12// With code page 65001:
13// ‰ RCDATA { "‰" L"‰" }
14// File encoded as Windows-1252:
15// ‰ => <U+FFFD REPLACEMENT CHARACTER> as u16
16// "‰" => 0x3F ('?')
17// L"‰" => <U+FFFD REPLACEMENT CHARACTER> as u16
18// File encoded as UTF-8:
19// ‰ => <U+2030 ‰> as u16
20// "‰" => 0x89 ('‰' encoded as Windows-1252)
21// L"‰" => <U+2030 ‰> as u16
22//
23// With code page 1252:
24// ‰ RCDATA { "‰" L"‰" }
25// File encoded as Windows-1252:
26// ‰ => <U+2030 ‰> as u16
27// "‰" => 0x89 ('‰' encoded as Windows-1252)
28// L"‰" => <U+2030 ‰> as u16
29// File encoded as UTF-8:
30// ‰ => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16
31// ^ first byte of utf8 representation
32// ^ second byte of UTF-8 representation (0x80), but interpretted as
33// Windows-1252 ('€') and then converted to UTF-16 (<U+20AC>)
34// ^ third byte of utf8 representation
35// "‰" => 0xE2, 0x80, 0xB0 (the bytes of the UTF-8 representation)
36// L"‰" => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 (see '‰ =>' explanation)
37//
38// With code page 1252:
39// <0x90> RCDATA { "<0x90>" L"<0x90>" }
40// File encoded as Windows-1252:
41// <0x90> => 0x90 as u16
42// "<0x90>" => 0x90
43// L"<0x90>" => 0x90 as u16
44// File encoded as UTF-8:
45// <0x90> => 0xC2 as u16, 0x90 as u16
46// "<0x90>" => 0xC2, 0x90 (the bytes of the UTF-8 representation of <U+0090>)
47// L"<0x90>" => 0xC2 as u16, 0x90 as u16
48//
49// Within a raw data block, file encoded as Windows-1252 (Â is <0xC2>):
50// "Âa" L"Âa" "\xC2ad" L"\xC2AD"
51// With code page 1252:
52// C2 61 C2 00 61 00 C2 61 64 AD C2
53// Â^ a^ Â~~~^ a~~~^ .^ a^ d^ ^~~~~\xC2AD
54// \xC2~`
55// With code page 65001:
56// 3F 61 FD FF 61 00 C2 61 64 AD C2
57// ^. a^ ^~~~. a~~~^ ^. a^ d^ ^~~~~\xC2AD
58// `. `. `~\xC2
59// `. `.~<0xC2>a is not well-formed UTF-8 (0xC2 expects a continutation byte after it).
60// `. Because 'a' is a valid first byte of a UTF-8 sequence, it is not included in the
61// `. invalid sequence so only the <0xC2> gets converted to <U+FFFD>.
62// `~Same as ^ but converted to '?' instead.
63//
64// Within a raw data block, file encoded as Windows-1252 (ð is <0xF0>, € is <0x80>):
65// "ð€a" L"ð€a"
66// With code page 1252:
67// F0 80 61 F0 00 AC 20 61 00
68// ð^ €^ a^ ð~~~^ €~~~^ a~~~^
69// With code page 65001:
70// 3F 61 FD FF 61 00
71// ^. a^ ^~~~. a~~~^
72// `. `.
73// `. `.~<0xF0><0x80> is not well-formed UTF-8, and <0x80> is not a valid first byte, so
74// `. both bytes are considered an invalid sequence and get converted to '<U+FFFD>'
75// `~Same as ^ but converted to '?' instead.
76
77/// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
78pub const CodePage = enum(u16) {
79 // supported
80 windows1252 = 1252, // windows-1252 ANSI Latin 1; Western European (Windows)
81 utf8 = 65001, // utf-8 Unicode (UTF-8)
82
83 // unsupported but valid
84 ibm037 = 37, // IBM037 IBM EBCDIC US-Canada
85 ibm437 = 437, // IBM437 OEM United States
86 ibm500 = 500, // IBM500 IBM EBCDIC International
87 asmo708 = 708, // ASMO-708 Arabic (ASMO 708)
88 asmo449plus = 709, // Arabic (ASMO-449+, BCON V4)
89 transparent_arabic = 710, // Arabic - Transparent Arabic
90 dos720 = 720, // DOS-720 Arabic (Transparent ASMO); Arabic (DOS)
91 ibm737 = 737, // ibm737 OEM Greek (formerly 437G); Greek (DOS)
92 ibm775 = 775, // ibm775 OEM Baltic; Baltic (DOS)
93 ibm850 = 850, // ibm850 OEM Multilingual Latin 1; Western European (DOS)
94 ibm852 = 852, // ibm852 OEM Latin 2; Central European (DOS)
95 ibm855 = 855, // IBM855 OEM Cyrillic (primarily Russian)
96 ibm857 = 857, // ibm857 OEM Turkish; Turkish (DOS)
97 ibm00858 = 858, // IBM00858 OEM Multilingual Latin 1 + Euro symbol
98 ibm860 = 860, // IBM860 OEM Portuguese; Portuguese (DOS)
99 ibm861 = 861, // ibm861 OEM Icelandic; Icelandic (DOS)
100 dos862 = 862, // DOS-862 OEM Hebrew; Hebrew (DOS)
101 ibm863 = 863, // IBM863 OEM French Canadian; French Canadian (DOS)
102 ibm864 = 864, // IBM864 OEM Arabic; Arabic (864)
103 ibm865 = 865, // IBM865 OEM Nordic; Nordic (DOS)
104 cp866 = 866, // cp866 OEM Russian; Cyrillic (DOS)
105 ibm869 = 869, // ibm869 OEM Modern Greek; Greek, Modern (DOS)
106 ibm870 = 870, // IBM870 IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2
107 windows874 = 874, // windows-874 Thai (Windows)
108 cp875 = 875, // cp875 IBM EBCDIC Greek Modern
109 shift_jis = 932, // shift_jis ANSI/OEM Japanese; Japanese (Shift-JIS)
110 gb2312 = 936, // gb2312 ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)
111 ks_c_5601_1987 = 949, // ks_c_5601-1987 ANSI/OEM Korean (Unified Hangul Code)
112 big5 = 950, // big5 ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5)
113 ibm1026 = 1026, // IBM1026 IBM EBCDIC Turkish (Latin 5)
114 ibm01047 = 1047, // IBM01047 IBM EBCDIC Latin 1/Open System
115 ibm01140 = 1140, // IBM01140 IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro)
116 ibm01141 = 1141, // IBM01141 IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro)
117 ibm01142 = 1142, // IBM01142 IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro)
118 ibm01143 = 1143, // IBM01143 IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro)
119 ibm01144 = 1144, // IBM01144 IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro)
120 ibm01145 = 1145, // IBM01145 IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro)
121 ibm01146 = 1146, // IBM01146 IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro)
122 ibm01147 = 1147, // IBM01147 IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro)
123 ibm01148 = 1148, // IBM01148 IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro)
124 ibm01149 = 1149, // IBM01149 IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro)
125 utf16 = 1200, // utf-16 Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications
126 utf16_fffe = 1201, // unicodeFFFE Unicode UTF-16, big endian byte order; available only to managed applications
127 windows1250 = 1250, // windows-1250 ANSI Central European; Central European (Windows)
128 windows1251 = 1251, // windows-1251 ANSI Cyrillic; Cyrillic (Windows)
129 windows1253 = 1253, // windows-1253 ANSI Greek; Greek (Windows)
130 windows1254 = 1254, // windows-1254 ANSI Turkish; Turkish (Windows)
131 windows1255 = 1255, // windows-1255 ANSI Hebrew; Hebrew (Windows)
132 windows1256 = 1256, // windows-1256 ANSI Arabic; Arabic (Windows)
133 windows1257 = 1257, // windows-1257 ANSI Baltic; Baltic (Windows)
134 windows1258 = 1258, // windows-1258 ANSI/OEM Vietnamese; Vietnamese (Windows)
135 johab = 1361, // Johab Korean (Johab)
136 macintosh = 10000, // macintosh MAC Roman; Western European (Mac)
137 x_mac_japanese = 10001, // x-mac-japanese Japanese (Mac)
138 x_mac_chinesetrad = 10002, // x-mac-chinesetrad MAC Traditional Chinese (Big5); Chinese Traditional (Mac)
139 x_mac_korean = 10003, // x-mac-korean Korean (Mac)
140 x_mac_arabic = 10004, // x-mac-arabic Arabic (Mac)
141 x_mac_hebrew = 10005, // x-mac-hebrew Hebrew (Mac)
142 x_mac_greek = 10006, // x-mac-greek Greek (Mac)
143 x_mac_cyrillic = 10007, // x-mac-cyrillic Cyrillic (Mac)
144 x_mac_chinesesimp = 10008, // x-mac-chinesesimp MAC Simplified Chinese (GB 2312); Chinese Simplified (Mac)
145 x_mac_romanian = 10010, // x-mac-romanian Romanian (Mac)
146 x_mac_ukranian = 10017, // x-mac-ukrainian Ukrainian (Mac)
147 x_mac_thai = 10021, // x-mac-thai Thai (Mac)
148 x_mac_ce = 10029, // x-mac-ce MAC Latin 2; Central European (Mac)
149 x_mac_icelandic = 10079, // x-mac-icelandic Icelandic (Mac)
150 x_mac_turkish = 10081, // x-mac-turkish Turkish (Mac)
151 x_mac_croatian = 10082, // x-mac-croatian Croatian (Mac)
152 utf32 = 12000, // utf-32 Unicode UTF-32, little endian byte order; available only to managed applications
153 utf32_be = 12001, // utf-32BE Unicode UTF-32, big endian byte order; available only to managed applications
154 x_chinese_cns = 20000, // x-Chinese_CNS CNS Taiwan; Chinese Traditional (CNS)
155 x_cp20001 = 20001, // x-cp20001 TCA Taiwan
156 x_chinese_eten = 20002, // x_Chinese-Eten Eten Taiwan; Chinese Traditional (Eten)
157 x_cp20003 = 20003, // x-cp20003 IBM5550 Taiwan
158 x_cp20004 = 20004, // x-cp20004 TeleText Taiwan
159 x_cp20005 = 20005, // x-cp20005 Wang Taiwan
160 x_ia5 = 20105, // x-IA5 IA5 (IRV International Alphabet No. 5, 7-bit); Western European (IA5)
161 x_ia5_german = 20106, // x-IA5-German IA5 German (7-bit)
162 x_ia5_swedish = 20107, // x-IA5-Swedish IA5 Swedish (7-bit)
163 x_ia5_norwegian = 20108, // x-IA5-Norwegian IA5 Norwegian (7-bit)
164 us_ascii = 20127, // us-ascii US-ASCII (7-bit)
165 x_cp20261 = 20261, // x-cp20261 T.61
166 x_cp20269 = 20269, // x-cp20269 ISO 6937 Non-Spacing Accent
167 ibm273 = 20273, // IBM273 IBM EBCDIC Germany
168 ibm277 = 20277, // IBM277 IBM EBCDIC Denmark-Norway
169 ibm278 = 20278, // IBM278 IBM EBCDIC Finland-Sweden
170 ibm280 = 20280, // IBM280 IBM EBCDIC Italy
171 ibm284 = 20284, // IBM284 IBM EBCDIC Latin America-Spain
172 ibm285 = 20285, // IBM285 IBM EBCDIC United Kingdom
173 ibm290 = 20290, // IBM290 IBM EBCDIC Japanese Katakana Extended
174 ibm297 = 20297, // IBM297 IBM EBCDIC France
175 ibm420 = 20420, // IBM420 IBM EBCDIC Arabic
176 ibm423 = 20423, // IBM423 IBM EBCDIC Greek
177 ibm424 = 20424, // IBM424 IBM EBCDIC Hebrew
178 x_ebcdic_korean_extended = 20833, // x-EBCDIC-KoreanExtended IBM EBCDIC Korean Extended
179 ibm_thai = 20838, // IBM-Thai IBM EBCDIC Thai
180 koi8_r = 20866, // koi8-r Russian (KOI8-R); Cyrillic (KOI8-R)
181 ibm871 = 20871, // IBM871 IBM EBCDIC Icelandic
182 ibm880 = 20880, // IBM880 IBM EBCDIC Cyrillic Russian
183 ibm905 = 20905, // IBM905 IBM EBCDIC Turkish
184 ibm00924 = 20924, // IBM00924 IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)
185 euc_jp_jis = 20932, // EUC-JP Japanese (JIS 0208-1990 and 0212-1990)
186 x_cp20936 = 20936, // x-cp20936 Simplified Chinese (GB2312); Chinese Simplified (GB2312-80)
187 x_cp20949 = 20949, // x-cp20949 Korean Wansung
188 cp1025 = 21025, // cp1025 IBM EBCDIC Cyrillic Serbian-Bulgarian
189 // = 21027, // (deprecated)
190 koi8_u = 21866, // koi8-u Ukrainian (KOI8-U); Cyrillic (KOI8-U)
191 iso8859_1 = 28591, // iso-8859-1 ISO 8859-1 Latin 1; Western European (ISO)
192 iso8859_2 = 28592, // iso-8859-2 ISO 8859-2 Central European; Central European (ISO)
193 iso8859_3 = 28593, // iso-8859-3 ISO 8859-3 Latin 3
194 iso8859_4 = 28594, // iso-8859-4 ISO 8859-4 Baltic
195 iso8859_5 = 28595, // iso-8859-5 ISO 8859-5 Cyrillic
196 iso8859_6 = 28596, // iso-8859-6 ISO 8859-6 Arabic
197 iso8859_7 = 28597, // iso-8859-7 ISO 8859-7 Greek
198 iso8859_8 = 28598, // iso-8859-8 ISO 8859-8 Hebrew; Hebrew (ISO-Visual)
199 iso8859_9 = 28599, // iso-8859-9 ISO 8859-9 Turkish
200 iso8859_13 = 28603, // iso-8859-13 ISO 8859-13 Estonian
201 iso8859_15 = 28605, // iso-8859-15 ISO 8859-15 Latin 9
202 x_europa = 29001, // x-Europa Europa 3
203 is8859_8_i = 38598, // iso-8859-8-i ISO 8859-8 Hebrew; Hebrew (ISO-Logical)
204 iso2022_jp = 50220, // iso-2022-jp ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS)
205 cs_iso2022_jp = 50221, // csISO2022JP ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana)
206 iso2022_jp_jis_x = 50222, // iso-2022-jp ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI)
207 iso2022_kr = 50225, // iso-2022-kr ISO 2022 Korean
208 x_cp50227 = 50227, // x-cp50227 ISO 2022 Simplified Chinese; Chinese Simplified (ISO 2022)
209 iso2022_chinesetrad = 50229, // ISO 2022 Traditional Chinese
210 ebcdic_jp_katakana_extended = 50930, // EBCDIC Japanese (Katakana) Extended
211 ebcdic_us_ca_jp = 50931, // EBCDIC US-Canada and Japanese
212 ebcdic_kr_extended = 50933, // EBCDIC Korean Extended and Korean
213 ebcdic_chinesesimp_extended = 50935, // EBCDIC Simplified Chinese Extended and Simplified Chinese
214 ebcdic_chinesesimp = 50936, // EBCDIC Simplified Chinese
215 ebcdic_us_ca_chinesetrad = 50937, // EBCDIC US-Canada and Traditional Chinese
216 ebcdic_jp_latin_extended = 50939, // EBCDIC Japanese (Latin) Extended and Japanese
217 euc_jp = 51932, // euc-jp EUC Japanese
218 euc_cn = 51936, // EUC-CN EUC Simplified Chinese; Chinese Simplified (EUC)
219 euc_kr = 51949, // euc-kr EUC Korean
220 euc_chinesetrad = 51950, // EUC Traditional Chinese
221 hz_gb2312 = 52936, // hz-gb-2312 HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)
222 gb18030 = 54936, // GB18030 Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030)
223 x_iscii_de = 57002, // x-iscii-de ISCII Devanagari
224 x_iscii_be = 57003, // x-iscii-be ISCII Bangla
225 x_iscii_ta = 57004, // x-iscii-ta ISCII Tamil
226 x_iscii_te = 57005, // x-iscii-te ISCII Telugu
227 x_iscii_as = 57006, // x-iscii-as ISCII Assamese
228 x_iscii_or = 57007, // x-iscii-or ISCII Odia
229 x_iscii_ka = 57008, // x-iscii-ka ISCII Kannada
230 x_iscii_ma = 57009, // x-iscii-ma ISCII Malayalam
231 x_iscii_gu = 57010, // x-iscii-gu ISCII Gujarati
232 x_iscii_pa = 57011, // x-iscii-pa ISCII Punjabi
233 utf7 = 65000, // utf-7 Unicode (UTF-7)
234
235 pub fn codepointAt(code_page: CodePage, index: usize, bytes: []const u8) ?Codepoint {
236 if (index >= bytes.len) return null;
237 switch (code_page) {
238 .windows1252 => {
239 // All byte values have a representation, so just convert the byte
240 return Codepoint{
241 .value = windows1252.toCodepoint(bytes[index]),
242 .byte_len = 1,
243 };
244 },
245 .utf8 => {
246 return Utf8.WellFormedDecoder.decode(bytes[index..]);
247 },
248 else => unreachable,
249 }
250 }
251
252 pub fn isSupported(code_page: CodePage) bool {
253 return switch (code_page) {
254 .windows1252, .utf8 => true,
255 else => false,
256 };
257 }
258
259 pub fn getByIdentifier(identifier: u16) !CodePage {
260 // There's probably a more efficient way to do this (e.g. ComptimeHashMap?) but
261 // this should be fine, especially since this function likely won't be called much.
262 inline for (@typeInfo(CodePage).Enum.fields) |enumField| {
263 if (identifier == enumField.value) {
264 return @field(CodePage, enumField.name);
265 }
266 }
267 return error.InvalidCodePage;
268 }
269
270 pub fn getByIdentifierEnsureSupported(identifier: u16) !CodePage {
271 const code_page = try getByIdentifier(identifier);
272 switch (isSupported(code_page)) {
273 true => return code_page,
274 false => return error.UnsupportedCodePage,
275 }
276 }
277};
278
279pub const Utf8 = struct {
280 /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section
281 /// D92 of Chapter 3 of the Unicode standard (Table 3-7 specifically).
282 pub const WellFormedDecoder = struct {
283 /// Like std.unicode.utf8ByteSequenceLength, but:
284 /// - Rejects non-well-formed first bytes, i.e. C0-C1, F5-FF
285 /// - Returns an optional value instead of an error union
286 pub fn sequenceLength(first_byte: u8) ?u3 {
287 return switch (first_byte) {
288 0x00...0x7F => 1,
289 0xC2...0xDF => 2,
290 0xE0...0xEF => 3,
291 0xF0...0xF4 => 4,
292 else => null,
293 };
294 }
295
296 fn isContinuationByte(byte: u8) bool {
297 return switch (byte) {
298 0x80...0xBF => true,
299 else => false,
300 };
301 }
302
303 pub fn decode(bytes: []const u8) Codepoint {
304 std.debug.assert(bytes.len > 0);
305 var first_byte = bytes[0];
306 var expected_len = sequenceLength(first_byte) orelse {
307 return .{ .value = Codepoint.invalid, .byte_len = 1 };
308 };
309 if (expected_len == 1) return .{ .value = first_byte, .byte_len = 1 };
310
311 var value: u21 = first_byte & 0b00011111;
312 var byte_index: u8 = 1;
313 while (byte_index < @min(bytes.len, expected_len)) : (byte_index += 1) {
314 const byte = bytes[byte_index];
315 // See Table 3-7 of D92 in Chapter 3 of the Unicode Standard
316 const valid: bool = switch (byte_index) {
317 1 => switch (first_byte) {
318 0xE0 => switch (byte) {
319 0xA0...0xBF => true,
320 else => false,
321 },
322 0xED => switch (byte) {
323 0x80...0x9F => true,
324 else => false,
325 },
326 0xF0 => switch (byte) {
327 0x90...0xBF => true,
328 else => false,
329 },
330 0xF4 => switch (byte) {
331 0x80...0x8F => true,
332 else => false,
333 },
334 else => switch (byte) {
335 0x80...0xBF => true,
336 else => false,
337 },
338 },
339 else => switch (byte) {
340 0x80...0xBF => true,
341 else => false,
342 },
343 };
344
345 if (!valid) {
346 var len = byte_index;
347 // Only include the byte in the invalid sequence if it's in the range
348 // of a continuation byte. All other values should not be included in the
349 // invalid sequence.
350 //
351 // Note: This is how the Windows RC compiler handles this, this may not
352 // be the correct-as-according-to-the-Unicode-standard way to do it.
353 if (isContinuationByte(byte)) len += 1;
354 return .{ .value = Codepoint.invalid, .byte_len = len };
355 }
356
357 value <<= 6;
358 value |= byte & 0b00111111;
359 }
360 if (byte_index != expected_len) {
361 return .{ .value = Codepoint.invalid, .byte_len = byte_index };
362 }
363 return .{ .value = value, .byte_len = expected_len };
364 }
365 };
366};
367
368test "Utf8.WellFormedDecoder" {
369 const invalid_utf8 = "\xF0\x80";
370 var decoded = Utf8.WellFormedDecoder.decode(invalid_utf8);
371 try std.testing.expectEqual(Codepoint.invalid, decoded.value);
372 try std.testing.expectEqual(@as(usize, 2), decoded.byte_len);
373}
374
375test "codepointAt invalid utf8" {
376 {
377 const invalid_utf8 = "\xf0\xf0\x80\x80\x80";
378 try std.testing.expectEqual(Codepoint{
379 .value = Codepoint.invalid,
380 .byte_len = 1,
381 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
382 try std.testing.expectEqual(Codepoint{
383 .value = Codepoint.invalid,
384 .byte_len = 2,
385 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);
386 try std.testing.expectEqual(Codepoint{
387 .value = Codepoint.invalid,
388 .byte_len = 1,
389 }, CodePage.utf8.codepointAt(3, invalid_utf8).?);
390 try std.testing.expectEqual(Codepoint{
391 .value = Codepoint.invalid,
392 .byte_len = 1,
393 }, CodePage.utf8.codepointAt(4, invalid_utf8).?);
394 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(5, invalid_utf8));
395 }
396
397 {
398 const invalid_utf8 = "\xE1\xA0\xC0";
399 try std.testing.expectEqual(Codepoint{
400 .value = Codepoint.invalid,
401 .byte_len = 2,
402 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
403 try std.testing.expectEqual(Codepoint{
404 .value = Codepoint.invalid,
405 .byte_len = 1,
406 }, CodePage.utf8.codepointAt(2, invalid_utf8).?);
407 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(3, invalid_utf8));
408 }
409
410 {
411 const invalid_utf8 = "\xD2";
412 try std.testing.expectEqual(Codepoint{
413 .value = Codepoint.invalid,
414 .byte_len = 1,
415 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
416 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, invalid_utf8));
417 }
418
419 {
420 const invalid_utf8 = "\xE1\xA0";
421 try std.testing.expectEqual(Codepoint{
422 .value = Codepoint.invalid,
423 .byte_len = 2,
424 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
425 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));
426 }
427
428 {
429 const invalid_utf8 = "\xC5\xFF";
430 try std.testing.expectEqual(Codepoint{
431 .value = Codepoint.invalid,
432 .byte_len = 1,
433 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
434 try std.testing.expectEqual(Codepoint{
435 .value = Codepoint.invalid,
436 .byte_len = 1,
437 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);
438 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));
439 }
440}
441
442test "codepointAt utf8 encoded" {
443 const utf8_encoded = "²";
444
445 // with code page utf8
446 try std.testing.expectEqual(Codepoint{
447 .value = '²',
448 .byte_len = 2,
449 }, CodePage.utf8.codepointAt(0, utf8_encoded).?);
450 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, utf8_encoded));
451
452 // with code page windows1252
453 try std.testing.expectEqual(Codepoint{
454 .value = '\xC2',
455 .byte_len = 1,
456 }, CodePage.windows1252.codepointAt(0, utf8_encoded).?);
457 try std.testing.expectEqual(Codepoint{
458 .value = '\xB2',
459 .byte_len = 1,
460 }, CodePage.windows1252.codepointAt(1, utf8_encoded).?);
461 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, utf8_encoded));
462}
463
464test "codepointAt windows1252 encoded" {
465 const windows1252_encoded = "\xB2";
466
467 // with code page utf8
468 try std.testing.expectEqual(Codepoint{
469 .value = Codepoint.invalid,
470 .byte_len = 1,
471 }, CodePage.utf8.codepointAt(0, windows1252_encoded).?);
472 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, windows1252_encoded));
473
474 // with code page windows1252
475 try std.testing.expectEqual(Codepoint{
476 .value = '\xB2',
477 .byte_len = 1,
478 }, CodePage.windows1252.codepointAt(0, windows1252_encoded).?);
479 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, windows1252_encoded));
480}
481
482pub const Codepoint = struct {
483 value: u21,
484 byte_len: usize,
485
486 pub const invalid: u21 = std.math.maxInt(u21);
487};
src/resinator/comments.zig created+340
......@@ -0,0 +1,340 @@
1//! Expects to run after a C preprocessor step that preserves comments.
2//!
3//! `rc` has a peculiar quirk where something like `blah/**/blah` will be
4//! transformed into `blahblah` during parsing. However, `clang -E` will
5//! transform it into `blah blah`, so in order to match `rc`, we need
6//! to remove comments ourselves after the preprocessor runs.
7//! Note: Multiline comments that actually span more than one line do
8//! get translated to a space character by `rc`.
9//!
10//! Removing comments before lexing also allows the lexer to not have to
11//! deal with comments which would complicate its implementation (this is something
12//! of a tradeoff, as removing comments in a separate pass means that we'll
13//! need to iterate the source twice instead of once, but having to deal with
14//! comments when lexing would be a pain).
15
16const std = @import("std");
17const Allocator = std.mem.Allocator;
18const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter;
19const SourceMappings = @import("source_mapping.zig").SourceMappings;
20const LineHandler = @import("lex.zig").LineHandler;
21const formsLineEndingPair = @import("source_mapping.zig").formsLineEndingPair;
22
23/// `buf` must be at least as long as `source`
24/// In-place transformation is supported (i.e. `source` and `buf` can be the same slice)
25pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMappings) []u8 {
26 std.debug.assert(buf.len >= source.len);
27 var result = UncheckedSliceWriter{ .slice = buf };
28 const State = enum {
29 start,
30 forward_slash,
31 line_comment,
32 multiline_comment,
33 multiline_comment_end,
34 single_quoted,
35 single_quoted_escape,
36 double_quoted,
37 double_quoted_escape,
38 };
39 var state: State = .start;
40 var index: usize = 0;
41 var pending_start: ?usize = null;
42 var line_handler = LineHandler{ .buffer = source };
43 while (index < source.len) : (index += 1) {
44 const c = source[index];
45 // TODO: Disallow \x1A, \x00, \x7F in comments. At least \x1A and \x00 can definitely
46 // cause errors or parsing weirdness in the Win32 RC compiler. These are disallowed
47 // in the lexer, but comments are stripped before getting to the lexer.
48 switch (state) {
49 .start => switch (c) {
50 '/' => {
51 state = .forward_slash;
52 pending_start = index;
53 },
54 '\r', '\n' => {
55 _ = line_handler.incrementLineNumber(index);
56 result.write(c);
57 },
58 else => {
59 switch (c) {
60 '"' => state = .double_quoted,
61 '\'' => state = .single_quoted,
62 else => {},
63 }
64 result.write(c);
65 },
66 },
67 .forward_slash => switch (c) {
68 '/' => state = .line_comment,
69 '*' => {
70 state = .multiline_comment;
71 },
72 else => {
73 _ = line_handler.maybeIncrementLineNumber(index);
74 result.writeSlice(source[pending_start.? .. index + 1]);
75 pending_start = null;
76 state = .start;
77 },
78 },
79 .line_comment => switch (c) {
80 '\r', '\n' => {
81 _ = line_handler.incrementLineNumber(index);
82 result.write(c);
83 state = .start;
84 },
85 else => {},
86 },
87 .multiline_comment => switch (c) {
88 '\r' => handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings),
89 '\n' => {
90 _ = line_handler.incrementLineNumber(index);
91 result.write(c);
92 },
93 '*' => state = .multiline_comment_end,
94 else => {},
95 },
96 .multiline_comment_end => switch (c) {
97 '\r' => {
98 handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings);
99 // We only want to treat this as a newline if it's part of a CRLF pair. If it's
100 // not, then we still want to stay in .multiline_comment_end, so that e.g. `*<\r>/` still
101 // functions as a `*/` comment ending. Kinda crazy, but that's how the Win32 implementation works.
102 if (formsLineEndingPair(source, '\r', index + 1)) {
103 state = .multiline_comment;
104 }
105 },
106 '\n' => {
107 _ = line_handler.incrementLineNumber(index);
108 result.write(c);
109 state = .multiline_comment;
110 },
111 '/' => {
112 state = .start;
113 },
114 else => {
115 state = .multiline_comment;
116 },
117 },
118 .single_quoted => switch (c) {
119 '\r', '\n' => {
120 _ = line_handler.incrementLineNumber(index);
121 state = .start;
122 result.write(c);
123 },
124 '\\' => {
125 state = .single_quoted_escape;
126 result.write(c);
127 },
128 '\'' => {
129 state = .start;
130 result.write(c);
131 },
132 else => {
133 result.write(c);
134 },
135 },
136 .single_quoted_escape => switch (c) {
137 '\r', '\n' => {
138 _ = line_handler.incrementLineNumber(index);
139 state = .start;
140 result.write(c);
141 },
142 else => {
143 state = .single_quoted;
144 result.write(c);
145 },
146 },
147 .double_quoted => switch (c) {
148 '\r', '\n' => {
149 _ = line_handler.incrementLineNumber(index);
150 state = .start;
151 result.write(c);
152 },
153 '\\' => {
154 state = .double_quoted_escape;
155 result.write(c);
156 },
157 '"' => {
158 state = .start;
159 result.write(c);
160 },
161 else => {
162 result.write(c);
163 },
164 },
165 .double_quoted_escape => switch (c) {
166 '\r', '\n' => {
167 _ = line_handler.incrementLineNumber(index);
168 state = .start;
169 result.write(c);
170 },
171 else => {
172 state = .double_quoted;
173 result.write(c);
174 },
175 },
176 }
177 }
178 return result.getWritten();
179}
180
181inline fn handleMultilineCarriageReturn(
182 source: []const u8,
183 line_handler: *LineHandler,
184 index: usize,
185 result: *UncheckedSliceWriter,
186 source_mappings: ?*SourceMappings,
187) void {
188 // Note: Bare \r within a multiline comment should *not* be treated as a line ending for the
189 // purposes of removing comments, but *should* be treated as a line ending for the
190 // purposes of line counting/source mapping
191 _ = line_handler.incrementLineNumber(index);
192 // So only write the \r if it's part of a CRLF pair
193 if (formsLineEndingPair(source, '\r', index + 1)) {
194 result.write('\r');
195 }
196 // And otherwise, we want to collapse the source mapping so that we can still know which
197 // line came from where.
198 else {
199 // Because the line gets collapsed, we need to decrement line number so that
200 // the next collapse acts on the first of the collapsed line numbers
201 line_handler.line_number -= 1;
202 if (source_mappings) |mappings| {
203 mappings.collapse(line_handler.line_number, 1);
204 }
205 }
206}
207
208pub fn removeCommentsAlloc(allocator: Allocator, source: []const u8, source_mappings: ?*SourceMappings) ![]u8 {
209 var buf = try allocator.alloc(u8, source.len);
210 errdefer allocator.free(buf);
211 var result = removeComments(source, buf, source_mappings);
212 return allocator.realloc(buf, result.len);
213}
214
215fn testRemoveComments(expected: []const u8, source: []const u8) !void {
216 const result = try removeCommentsAlloc(std.testing.allocator, source, null);
217 defer std.testing.allocator.free(result);
218
219 try std.testing.expectEqualStrings(expected, result);
220}
221
222test "basic" {
223 try testRemoveComments("", "// comment");
224 try testRemoveComments("", "/* comment */");
225}
226
227test "mixed" {
228 try testRemoveComments("hello", "hello// comment");
229 try testRemoveComments("hello", "hel/* comment */lo");
230}
231
232test "within a string" {
233 // escaped " is \"
234 try testRemoveComments(
235 \\blah"//som\"/*ething*/"BLAH
236 ,
237 \\blah"//som\"/*ething*/"BLAH
238 );
239}
240
241test "line comments retain newlines" {
242 try testRemoveComments(
243 \\
244 \\
245 \\
246 ,
247 \\// comment
248 \\// comment
249 \\// comment
250 );
251
252 try testRemoveComments("\r\n", "//comment\r\n");
253}
254
255test "crazy" {
256 try testRemoveComments(
257 \\blah"/*som*/\""BLAH
258 ,
259 \\blah"/*som*/\""/*ething*/BLAH
260 );
261
262 try testRemoveComments(
263 \\blah"/*som*/"BLAH RCDATA "BEGIN END
264 \\
265 \\
266 \\hello
267 \\"
268 ,
269 \\blah"/*som*/"/*ething*/BLAH RCDATA "BEGIN END
270 \\// comment
271 \\//"blah blah" RCDATA {}
272 \\hello
273 \\"
274 );
275}
276
277test "multiline comment with newlines" {
278 // bare \r is not treated as a newline
279 try testRemoveComments("blahblah", "blah/*some\rthing*/blah");
280
281 try testRemoveComments(
282 \\blah
283 \\blah
284 ,
285 \\blah/*some
286 \\thing*/blah
287 );
288 try testRemoveComments(
289 "blah\r\nblah",
290 "blah/*some\r\nthing*/blah",
291 );
292
293 // handle *<not /> correctly
294 try testRemoveComments(
295 \\blah
296 \\
297 \\
298 ,
299 \\blah/*some
300 \\thing*
301 \\/bl*ah*/
302 );
303}
304
305test "comments appended to a line" {
306 try testRemoveComments(
307 \\blah
308 \\blah
309 ,
310 \\blah // line comment
311 \\blah
312 );
313 try testRemoveComments(
314 "blah \r\nblah",
315 "blah // line comment\r\nblah",
316 );
317}
318
319test "remove comments with mappings" {
320 const allocator = std.testing.allocator;
321 var mut_source = "blah/*\rcommented line*\r/blah".*;
322 var mappings = SourceMappings{};
323 _ = try mappings.files.put(allocator, "test.rc");
324 try mappings.set(allocator, 1, .{ .start_line = 1, .end_line = 1, .filename_offset = 0 });
325 try mappings.set(allocator, 2, .{ .start_line = 2, .end_line = 2, .filename_offset = 0 });
326 try mappings.set(allocator, 3, .{ .start_line = 3, .end_line = 3, .filename_offset = 0 });
327 defer mappings.deinit(allocator);
328
329 var result = removeComments(&mut_source, &mut_source, &mappings);
330
331 try std.testing.expectEqualStrings("blahblah", result);
332 try std.testing.expectEqual(@as(usize, 1), mappings.mapping.items.len);
333 try std.testing.expectEqual(@as(usize, 3), mappings.mapping.items[0].end_line);
334}
335
336test "in place" {
337 var mut_source = "blah /* comment */ blah".*;
338 var result = removeComments(&mut_source, &mut_source, null);
339 try std.testing.expectEqualStrings("blah blah", result);
340}
src/resinator/compile.zig created+3356
......@@ -0,0 +1,3356 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const Node = @import("ast.zig").Node;
5const lex = @import("lex.zig");
6const Parser = @import("parse.zig").Parser;
7const Resource = @import("rc.zig").Resource;
8const Token = @import("lex.zig").Token;
9const literals = @import("literals.zig");
10const Number = literals.Number;
11const SourceBytes = literals.SourceBytes;
12const Diagnostics = @import("errors.zig").Diagnostics;
13const ErrorDetails = @import("errors.zig").ErrorDetails;
14const MemoryFlags = @import("res.zig").MemoryFlags;
15const rc = @import("rc.zig");
16const res = @import("res.zig");
17const ico = @import("ico.zig");
18const ani = @import("ani.zig");
19const bmp = @import("bmp.zig");
20const WORD = std.os.windows.WORD;
21const DWORD = std.os.windows.DWORD;
22const utils = @import("utils.zig");
23const NameOrOrdinal = res.NameOrOrdinal;
24const CodePage = @import("code_pages.zig").CodePage;
25const CodePageLookup = @import("ast.zig").CodePageLookup;
26const SourceMappings = @import("source_mapping.zig").SourceMappings;
27const windows1252 = @import("windows1252.zig");
28const lang = @import("lang.zig");
29const code_pages = @import("code_pages.zig");
30const errors = @import("errors.zig");
31
32pub const CompileOptions = struct {
33 cwd: std.fs.Dir,
34 diagnostics: *Diagnostics,
35 source_mappings: ?*SourceMappings = null,
36 /// List of paths (absolute or relative to `cwd`) for every file that the resources within the .rc file depend on.
37 /// Items within the list will be allocated using the allocator of the ArrayList and must be
38 /// freed by the caller.
39 /// TODO: Maybe a dedicated struct for this purpose so that it's a bit nicer to work with.
40 dependencies_list: ?*std.ArrayList([]const u8) = null,
41 default_code_page: CodePage = .windows1252,
42 ignore_include_env_var: bool = false,
43 extra_include_paths: []const []const u8 = &.{},
44 /// This is just an API convenience to allow separately passing 'system' (i.e. those
45 /// that would normally be gotten from the INCLUDE env var) include paths. This is mostly
46 /// intended for use when setting `ignore_include_env_var = true`. When `ignore_include_env_var`
47 /// is false, `system_include_paths` will be searched before the paths in the INCLUDE env var.
48 system_include_paths: []const []const u8 = &.{},
49 default_language_id: ?u16 = null,
50 // TODO: Implement verbose output
51 verbose: bool = false,
52 null_terminate_string_table_strings: bool = false,
53 /// Note: This is a u15 to ensure that the maximum number of UTF-16 code units
54 /// plus a null-terminator can always fit into a u16.
55 max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
56 silent_duplicate_control_ids: bool = false,
57 warn_instead_of_error_on_invalid_code_page: bool = false,
58};
59
60pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, options: CompileOptions) !void {
61 var lexer = lex.Lexer.init(source, .{
62 .default_code_page = options.default_code_page,
63 .source_mappings = options.source_mappings,
64 .max_string_literal_codepoints = options.max_string_literal_codepoints,
65 });
66 var parser = Parser.init(&lexer, .{
67 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
68 });
69 var tree = try parser.parse(allocator, options.diagnostics);
70 defer tree.deinit();
71
72 var search_dirs = std.ArrayList(SearchDir).init(allocator);
73 defer {
74 for (search_dirs.items) |*search_dir| {
75 search_dir.deinit(allocator);
76 }
77 search_dirs.deinit();
78 }
79
80 if (options.source_mappings) |source_mappings| {
81 const root_path = source_mappings.files.get(source_mappings.root_filename_offset);
82 // If dirname returns null, then the root path will be the same as
83 // the cwd so we don't need to add it as a distinct search path.
84 if (std.fs.path.dirname(root_path)) |root_dir_path| {
85 var root_dir = try options.cwd.openDir(root_dir_path, .{});
86 errdefer root_dir.close();
87 try search_dirs.append(.{ .dir = root_dir, .path = try allocator.dupe(u8, root_dir_path) });
88 }
89 }
90 // Re-open the passed in cwd since we want to be able to close it (std.fs.cwd() shouldn't be closed)
91 // `catch unreachable` since `options.cwd` is expected to be a valid dir handle, so opening
92 // a new handle to it should be fine as well.
93 // TODO: Maybe catch and return an error instead
94 const cwd_dir = options.cwd.openDir(".", .{}) catch unreachable;
95 try search_dirs.append(.{ .dir = cwd_dir, .path = null });
96 for (options.extra_include_paths) |extra_include_path| {
97 var dir = openSearchPathDir(options.cwd, extra_include_path) catch {
98 // TODO: maybe a warning that the search path is skipped?
99 continue;
100 };
101 errdefer dir.close();
102 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, extra_include_path) });
103 }
104 for (options.system_include_paths) |system_include_path| {
105 var dir = openSearchPathDir(options.cwd, system_include_path) catch {
106 // TODO: maybe a warning that the search path is skipped?
107 continue;
108 };
109 errdefer dir.close();
110 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, system_include_path) });
111 }
112 if (!options.ignore_include_env_var) {
113 const INCLUDE = std.process.getEnvVarOwned(allocator, "INCLUDE") catch "";
114 defer allocator.free(INCLUDE);
115
116 // TODO: Should this be platform-specific? How does windres/llvm-rc handle this (if at all)?
117 var it = std.mem.tokenize(u8, INCLUDE, ";");
118 while (it.next()) |search_path| {
119 var dir = openSearchPathDir(options.cwd, search_path) catch continue;
120 errdefer dir.close();
121 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, search_path) });
122 }
123 }
124
125 var arena_allocator = std.heap.ArenaAllocator.init(allocator);
126 defer arena_allocator.deinit();
127 const arena = arena_allocator.allocator();
128
129 var compiler = Compiler{
130 .source = source,
131 .arena = arena,
132 .allocator = allocator,
133 .cwd = options.cwd,
134 .diagnostics = options.diagnostics,
135 .dependencies_list = options.dependencies_list,
136 .input_code_pages = &tree.input_code_pages,
137 .output_code_pages = &tree.output_code_pages,
138 // This is only safe because we know search_dirs won't be modified past this point
139 .search_dirs = search_dirs.items,
140 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
141 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
142 };
143 if (options.default_language_id) |default_language_id| {
144 compiler.state.language = res.Language.fromInt(default_language_id);
145 }
146
147 try compiler.writeRoot(tree.root(), writer);
148}
149
150pub const Compiler = struct {
151 source: []const u8,
152 arena: Allocator,
153 allocator: Allocator,
154 cwd: std.fs.Dir,
155 state: State = .{},
156 diagnostics: *Diagnostics,
157 dependencies_list: ?*std.ArrayList([]const u8),
158 input_code_pages: *const CodePageLookup,
159 output_code_pages: *const CodePageLookup,
160 search_dirs: []SearchDir,
161 null_terminate_string_table_strings: bool,
162 silent_duplicate_control_ids: bool,
163
164 pub const State = struct {
165 icon_id: u16 = 1,
166 string_tables: StringTablesByLanguage = .{},
167 language: res.Language = .{},
168 font_dir: FontDir = .{},
169 version: u32 = 0,
170 characteristics: u32 = 0,
171 };
172
173 pub fn writeRoot(self: *Compiler, root: *Node.Root, writer: anytype) !void {
174 try writeEmptyResource(writer);
175 for (root.body) |node| {
176 try self.writeNode(node, writer);
177 }
178
179 // now write the FONTDIR (if it has anything in it)
180 try self.state.font_dir.writeResData(self, writer);
181 if (self.state.font_dir.fonts.items.len != 0) {
182 // The Win32 RC compiler may write a different FONTDIR resource than us,
183 // due to it sometimes writing a non-zero-length device name/face name
184 // whereas we *always* write them both as zero-length.
185 //
186 // In practical terms, this doesn't matter, since for various reasons the format
187 // of the FONTDIR cannot be relied on and is seemingly not actually used by anything
188 // anymore. We still want to emit some sort of diagnostic for the purposes of being able
189 // to know that our .RES is intentionally not meant to be byte-for-byte identical with
190 // the rc.exe output.
191 //
192 // By using the hint type here, we allow this diagnostic to be detected in code,
193 // but it will not be printed since the end-user doesn't need to care.
194 try self.addErrorDetails(.{
195 .err = .result_contains_fontdir,
196 .type = .hint,
197 .token = undefined,
198 });
199 }
200 // once we've written every else out, we can write out the finalized STRINGTABLE resources
201 var string_tables_it = self.state.string_tables.tables.iterator();
202 while (string_tables_it.next()) |string_table_entry| {
203 var string_table_it = string_table_entry.value_ptr.blocks.iterator();
204 while (string_table_it.next()) |entry| {
205 try entry.value_ptr.writeResData(self, string_table_entry.key_ptr.*, entry.key_ptr.*, writer);
206 }
207 }
208 }
209
210 pub fn writeNode(self: *Compiler, node: *Node, writer: anytype) !void {
211 switch (node.id) {
212 .root => unreachable, // writeRoot should be called directly instead
213 .resource_external => try self.writeResourceExternal(@fieldParentPtr(Node.ResourceExternal, "base", node), writer),
214 .resource_raw_data => try self.writeResourceRawData(@fieldParentPtr(Node.ResourceRawData, "base", node), writer),
215 .literal => unreachable, // this is context dependent and should be handled by its parent
216 .binary_expression => unreachable,
217 .grouped_expression => unreachable,
218 .not_expression => unreachable,
219 .invalid => {}, // no-op, currently only used for dangling literals at EOF
220 .accelerators => try self.writeAccelerators(@fieldParentPtr(Node.Accelerators, "base", node), writer),
221 .accelerator => unreachable, // handled by writeAccelerators
222 .dialog => try self.writeDialog(@fieldParentPtr(Node.Dialog, "base", node), writer),
223 .control_statement => unreachable,
224 .toolbar => try self.writeToolbar(@fieldParentPtr(Node.Toolbar, "base", node), writer),
225 .menu => try self.writeMenu(@fieldParentPtr(Node.Menu, "base", node), writer),
226 .menu_item => unreachable,
227 .menu_item_separator => unreachable,
228 .menu_item_ex => unreachable,
229 .popup => unreachable,
230 .popup_ex => unreachable,
231 .version_info => try self.writeVersionInfo(@fieldParentPtr(Node.VersionInfo, "base", node), writer),
232 .version_statement => unreachable,
233 .block => unreachable,
234 .block_value => unreachable,
235 .block_value_value => unreachable,
236 .string_table => try self.writeStringTable(@fieldParentPtr(Node.StringTable, "base", node)),
237 .string_table_string => unreachable, // handled by writeStringTable
238 .language_statement => self.writeLanguageStatement(@fieldParentPtr(Node.LanguageStatement, "base", node)),
239 .font_statement => unreachable,
240 .simple_statement => self.writeTopLevelSimpleStatement(@fieldParentPtr(Node.SimpleStatement, "base", node)),
241 }
242 }
243
244 /// Returns the filename encoded as UTF-8 (allocated by self.allocator)
245 pub fn evaluateFilenameExpression(self: *Compiler, expression_node: *Node) ![]u8 {
246 switch (expression_node.id) {
247 .literal => {
248 const literal_node = expression_node.cast(.literal).?;
249 switch (literal_node.token.id) {
250 .literal, .number => {
251 const slice = literal_node.token.slice(self.source);
252 const code_page = self.input_code_pages.getForToken(literal_node.token);
253 var buf = try std.ArrayList(u8).initCapacity(self.allocator, slice.len);
254 errdefer buf.deinit();
255
256 var index: usize = 0;
257 while (code_page.codepointAt(index, slice)) |codepoint| : (index += codepoint.byte_len) {
258 const c = codepoint.value;
259 if (c == code_pages.Codepoint.invalid) {
260 try buf.appendSlice("�");
261 } else {
262 // Anything that is not returned as an invalid codepoint must be encodable as UTF-8.
263 const utf8_len = std.unicode.utf8CodepointSequenceLength(c) catch unreachable;
264 try buf.ensureUnusedCapacity(utf8_len);
265 _ = std.unicode.utf8Encode(c, buf.unusedCapacitySlice()) catch unreachable;
266 buf.items.len += utf8_len;
267 }
268 }
269
270 return buf.toOwnedSlice();
271 },
272 .quoted_ascii_string, .quoted_wide_string => {
273 const slice = literal_node.token.slice(self.source);
274 const column = literal_node.token.calculateColumn(self.source, 8, null);
275 const bytes = SourceBytes{ .slice = slice, .code_page = self.input_code_pages.getForToken(literal_node.token) };
276
277 var buf = std.ArrayList(u8).init(self.allocator);
278 errdefer buf.deinit();
279
280 // Filenames are sort-of parsed as if they were wide strings, but the max escape width of
281 // hex/octal escapes is still determined by the L prefix. Since we want to end up with
282 // UTF-8, we can parse either string type directly to UTF-8.
283 var parser = literals.IterativeStringParser.init(bytes, .{
284 .start_column = column,
285 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
286 });
287
288 while (try parser.nextUnchecked()) |parsed| {
289 const c = parsed.codepoint;
290 if (c == code_pages.Codepoint.invalid) {
291 try buf.appendSlice("�");
292 } else {
293 var codepoint_buf: [4]u8 = undefined;
294 // If the codepoint cannot be encoded, we fall back to �
295 if (std.unicode.utf8Encode(c, &codepoint_buf)) |len| {
296 try buf.appendSlice(codepoint_buf[0..len]);
297 } else |_| {
298 try buf.appendSlice("�");
299 }
300 }
301 }
302
303 return buf.toOwnedSlice();
304 },
305 else => {
306 std.debug.print("unexpected filename token type: {}\n", .{literal_node.token});
307 unreachable; // no other token types should be in a filename literal node
308 },
309 }
310 },
311 .binary_expression => {
312 const binary_expression_node = expression_node.cast(.binary_expression).?;
313 return self.evaluateFilenameExpression(binary_expression_node.right);
314 },
315 .grouped_expression => {
316 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
317 return self.evaluateFilenameExpression(grouped_expression_node.expression);
318 },
319 else => unreachable,
320 }
321 }
322
323 /// https://learn.microsoft.com/en-us/windows/win32/menurc/searching-for-files
324 ///
325 /// Searches, in this order:
326 /// Directory of the 'root' .rc file (if different from CWD)
327 /// CWD
328 /// extra_include_paths (resolved relative to CWD)
329 /// system_include_paths (resolve relative to CWD)
330 /// INCLUDE environment var paths (only if ignore_include_env_var is false; resolved relative to CWD)
331 ///
332 /// Note: The CWD being searched *in addition to* the directory of the 'root' .rc file
333 /// is also how the Win32 RC compiler preprocessor searches for includes, but that
334 /// differs from how the clang preprocessor searches for includes.
335 ///
336 /// Note: This will always return the first matching file that can be opened.
337 /// This matches the Win32 RC compiler, which will fail with an error if the first
338 /// matching file is invalid. That is, it does not do the `cmd` PATH searching
339 /// thing of continuing to look for matching files until it finds a valid
340 /// one if a matching file is invalid.
341 fn searchForFile(self: *Compiler, path: []const u8) !std.fs.File {
342 // If the path is absolute, then it is not resolved relative to any search
343 // paths, so there's no point in checking them.
344 //
345 // This behavior was determined/confirmed with the following test:
346 // - A `test.rc` file with the contents `1 RCDATA "/test.bin"`
347 // - A `test.bin` file at `C:\test.bin`
348 // - A `test.bin` file at `inc\test.bin` relative to the .rc file
349 // - Invoking `rc` with `rc /i inc test.rc`
350 //
351 // This results in a .res file with the contents of `C:\test.bin`, not
352 // the contents of `inc\test.bin`. Further, if `C:\test.bin` is deleted,
353 // then it start failing to find `/test.bin`, meaning that it does not resolve
354 // `/test.bin` relative to include paths and instead only treats it as
355 // an absolute path.
356 if (std.fs.path.isAbsolute(path)) {
357 const file = try utils.openFileNotDir(std.fs.cwd(), path, .{});
358 errdefer file.close();
359
360 if (self.dependencies_list) |dependencies_list| {
361 const duped_path = try dependencies_list.allocator.dupe(u8, path);
362 errdefer dependencies_list.allocator.free(duped_path);
363 try dependencies_list.append(duped_path);
364 }
365 }
366
367 var first_error: ?std.fs.File.OpenError = null;
368 for (self.search_dirs) |search_dir| {
369 if (utils.openFileNotDir(search_dir.dir, path, .{})) |file| {
370 errdefer file.close();
371
372 if (self.dependencies_list) |dependencies_list| {
373 const searched_file_path = try std.fs.path.join(dependencies_list.allocator, &.{
374 search_dir.path orelse "", path,
375 });
376 errdefer dependencies_list.allocator.free(searched_file_path);
377 try dependencies_list.append(searched_file_path);
378 }
379
380 return file;
381 } else |err| if (first_error == null) {
382 first_error = err;
383 }
384 }
385 return first_error orelse error.FileNotFound;
386 }
387
388 pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: anytype) !void {
389 // Init header with data size zero for now, will need to fill it in later
390 var header = try self.resourceHeader(node.id, node.type, .{});
391 defer header.deinit(self.allocator);
392
393 const maybe_predefined_type = header.predefinedResourceType();
394
395 // DLGINCLUDE has special handling that doesn't actually need the file to exist
396 if (maybe_predefined_type != null and maybe_predefined_type.? == .DLGINCLUDE) {
397 const filename_token = node.filename.cast(.literal).?.token;
398 const parsed_filename = try self.parseQuotedStringAsAsciiString(filename_token);
399 defer self.allocator.free(parsed_filename);
400
401 header.applyMemoryFlags(node.common_resource_attributes, self.source);
402 header.data_size = @intCast(parsed_filename.len + 1);
403 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
404 try writer.writeAll(parsed_filename);
405 try writer.writeByte(0);
406 try writeDataPadding(writer, header.data_size);
407 return;
408 }
409
410 const filename_utf8 = try self.evaluateFilenameExpression(node.filename);
411 defer self.allocator.free(filename_utf8);
412
413 // TODO: More robust checking of the validity of the filename.
414 // This currently only checks for NUL bytes, but it should probably also check for
415 // platform-specific invalid characters like '*', '?', '"', '<', '>', '|' (Windows)
416 // Related: https://github.com/ziglang/zig/pull/14533#issuecomment-1416888193
417 if (std.mem.indexOfScalar(u8, filename_utf8, 0) != null) {
418 return self.addErrorDetailsAndFail(.{
419 .err = .invalid_filename,
420 .token = node.filename.getFirstToken(),
421 .token_span_end = node.filename.getLastToken(),
422 .extra = .{ .number = 0 },
423 });
424 }
425
426 // Allow plain number literals, but complex number expressions are evaluated strangely
427 // and almost certainly lead to things not intended by the user (e.g. '(1+-1)' evaluates
428 // to the filename '-1'), so error if the filename node is a grouped/binary expression.
429 // Note: This is done here instead of during parsing so that we can easily include
430 // the evaluated filename as part of the error messages.
431 if (node.filename.id != .literal) {
432 const filename_string_index = try self.diagnostics.putString(filename_utf8);
433 try self.addErrorDetails(.{
434 .err = .number_expression_as_filename,
435 .token = node.filename.getFirstToken(),
436 .token_span_end = node.filename.getLastToken(),
437 .extra = .{ .number = filename_string_index },
438 });
439 return self.addErrorDetailsAndFail(.{
440 .err = .number_expression_as_filename,
441 .type = .note,
442 .token = node.filename.getFirstToken(),
443 .token_span_end = node.filename.getLastToken(),
444 .print_source_line = false,
445 .extra = .{ .number = filename_string_index },
446 });
447 }
448 // From here on out, we know that the filename must be comprised of a single token,
449 // so get it here to simplify future usage.
450 const filename_token = node.filename.getFirstToken();
451
452 const file = self.searchForFile(filename_utf8) catch |err| switch (err) {
453 error.OutOfMemory => |e| return e,
454 else => |e| {
455 const filename_string_index = try self.diagnostics.putString(filename_utf8);
456 return self.addErrorDetailsAndFail(.{
457 .err = .file_open_error,
458 .token = filename_token,
459 .extra = .{ .file_open_error = .{
460 .err = ErrorDetails.FileOpenError.enumFromError(e),
461 .filename_string_index = filename_string_index,
462 } },
463 });
464 },
465 };
466 defer file.close();
467
468 if (maybe_predefined_type) |predefined_type| {
469 switch (predefined_type) {
470 .GROUP_ICON, .GROUP_CURSOR => {
471 // Check for animated icon first
472 if (ani.isAnimatedIcon(file.reader())) {
473 // Animated icons are just put into the resource unmodified,
474 // and the resource type changes to ANIICON/ANICURSOR
475
476 const new_predefined_type: res.RT = switch (predefined_type) {
477 .GROUP_ICON => .ANIICON,
478 .GROUP_CURSOR => .ANICURSOR,
479 else => unreachable,
480 };
481 header.type_value.ordinal = @intFromEnum(new_predefined_type);
482 header.memory_flags = MemoryFlags.defaults(new_predefined_type);
483 header.applyMemoryFlags(node.common_resource_attributes, self.source);
484 header.data_size = @intCast(try file.getEndPos());
485
486 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
487 try file.seekTo(0);
488 try writeResourceData(writer, file.reader(), header.data_size);
489 return;
490 }
491
492 // isAnimatedIcon moved the file cursor so reset to the start
493 try file.seekTo(0);
494
495 const icon_dir = ico.read(self.allocator, file.reader(), try file.getEndPos()) catch |err| switch (err) {
496 error.OutOfMemory => |e| return e,
497 else => |e| {
498 return self.iconReadError(
499 e,
500 filename_utf8,
501 filename_token,
502 predefined_type,
503 );
504 },
505 };
506 defer icon_dir.deinit();
507
508 // This limit is inherent to the ico format since number of entries is a u16 field.
509 std.debug.assert(icon_dir.entries.len <= std.math.maxInt(u16));
510
511 // Note: The Win32 RC compiler will compile the resource as whatever type is
512 // in the icon_dir regardless of the type of resource specified in the .rc.
513 // This leads to unusable .res files when the types mismatch, so
514 // we error instead.
515 const res_types_match = switch (predefined_type) {
516 .GROUP_ICON => icon_dir.image_type == .icon,
517 .GROUP_CURSOR => icon_dir.image_type == .cursor,
518 else => unreachable,
519 };
520 if (!res_types_match) {
521 return self.addErrorDetailsAndFail(.{
522 .err = .icon_dir_and_resource_type_mismatch,
523 .token = filename_token,
524 .extra = .{ .resource = switch (predefined_type) {
525 .GROUP_ICON => .icon,
526 .GROUP_CURSOR => .cursor,
527 else => unreachable,
528 } },
529 });
530 }
531
532 // Memory flags affect the RT_ICON and the RT_GROUP_ICON differently
533 var icon_memory_flags = MemoryFlags.defaults(res.RT.ICON);
534 applyToMemoryFlags(&icon_memory_flags, node.common_resource_attributes, self.source);
535 applyToGroupMemoryFlags(&header.memory_flags, node.common_resource_attributes, self.source);
536
537 const first_icon_id = self.state.icon_id;
538 const entry_type = if (predefined_type == .GROUP_ICON) @intFromEnum(res.RT.ICON) else @intFromEnum(res.RT.CURSOR);
539 for (icon_dir.entries, 0..) |*entry, entry_i_usize| {
540 // We know that the entry index must fit within a u16, so
541 // cast it here to simplify usage sites.
542 const entry_i: u16 = @intCast(entry_i_usize);
543 var full_data_size = entry.data_size_in_bytes;
544 if (icon_dir.image_type == .cursor) {
545 full_data_size = std.math.add(u32, full_data_size, 4) catch {
546 return self.addErrorDetailsAndFail(.{
547 .err = .resource_data_size_exceeds_max,
548 .token = node.id,
549 });
550 };
551 }
552
553 const image_header = ResourceHeader{
554 .type_value = .{ .ordinal = entry_type },
555 .name_value = .{ .ordinal = self.state.icon_id },
556 .data_size = full_data_size,
557 .memory_flags = icon_memory_flags,
558 .language = self.state.language,
559 .version = self.state.version,
560 .characteristics = self.state.characteristics,
561 };
562 try image_header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
563
564 // From https://learn.microsoft.com/en-us/windows/win32/menurc/localheader:
565 // > The LOCALHEADER structure is the first data written to the RT_CURSOR
566 // > resource if a RESDIR structure contains information about a cursor.
567 // where LOCALHEADER is `struct { WORD xHotSpot; WORD yHotSpot; }`
568 if (icon_dir.image_type == .cursor) {
569 try writer.writeIntLittle(u16, entry.type_specific_data.cursor.hotspot_x);
570 try writer.writeIntLittle(u16, entry.type_specific_data.cursor.hotspot_y);
571 }
572
573 try file.seekTo(entry.data_offset_from_start_of_file);
574 const header_bytes = file.reader().readBytesNoEof(16) catch {
575 return self.iconReadError(
576 error.UnexpectedEOF,
577 filename_utf8,
578 filename_token,
579 predefined_type,
580 );
581 };
582
583 const image_format = ico.ImageFormat.detect(&header_bytes);
584 if (!image_format.validate(&header_bytes)) {
585 return self.iconReadError(
586 error.InvalidHeader,
587 filename_utf8,
588 filename_token,
589 predefined_type,
590 );
591 }
592 switch (image_format) {
593 .riff => switch (icon_dir.image_type) {
594 .icon => {
595 // The Win32 RC compiler treats this as an error, but icon dirs
596 // with RIFF encoded icons within them work ~okay (they work
597 // in some places but not others, they may not animate, etc) if they are
598 // allowed to be compiled.
599 try self.addErrorDetails(.{
600 .err = .rc_would_error_on_icon_dir,
601 .type = .warning,
602 .token = filename_token,
603 .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
604 });
605 try self.addErrorDetails(.{
606 .err = .rc_would_error_on_icon_dir,
607 .type = .note,
608 .print_source_line = false,
609 .token = filename_token,
610 .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
611 });
612 },
613 .cursor => {
614 // The Win32 RC compiler errors in this case too, but we only error
615 // here because the cursor would fail to be loaded at runtime if we
616 // compiled it.
617 return self.addErrorDetailsAndFail(.{
618 .err = .format_not_supported_in_icon_dir,
619 .token = filename_token,
620 .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .riff, .index = entry_i } },
621 });
622 },
623 },
624 .png => switch (icon_dir.image_type) {
625 .icon => {
626 // PNG always seems to have 1 for color planes no matter what
627 entry.type_specific_data.icon.color_planes = 1;
628 // These seem to be the only values of num_colors that
629 // get treated specially
630 entry.type_specific_data.icon.bits_per_pixel = switch (entry.num_colors) {
631 2 => 1,
632 8 => 3,
633 16 => 4,
634 else => entry.type_specific_data.icon.bits_per_pixel,
635 };
636 },
637 .cursor => {
638 // The Win32 RC compiler treats this as an error, but cursor dirs
639 // with PNG encoded icons within them work fine if they are
640 // allowed to be compiled.
641 try self.addErrorDetails(.{
642 .err = .rc_would_error_on_icon_dir,
643 .type = .warning,
644 .token = filename_token,
645 .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .png, .index = entry_i } },
646 });
647 },
648 },
649 .dib => {
650 const bitmap_header: *const ico.BitmapHeader = @ptrCast(@alignCast(&header_bytes));
651 const bitmap_version = ico.BitmapHeader.Version.get(std.mem.littleToNative(u32, bitmap_header.bcSize));
652
653 // The Win32 RC compiler only allows headers with
654 // `bcSize == sizeof(BITMAPINFOHEADER)`, but it seems unlikely
655 // that there's a good reason for that outside of too-old
656 // bitmap headers.
657 // TODO: Need to test V4 and V5 bitmaps to check they actually work
658 if (bitmap_version == .@"win2.0") {
659 return self.addErrorDetailsAndFail(.{
660 .err = .rc_would_error_on_bitmap_version,
661 .token = filename_token,
662 .extra = .{ .icon_dir = .{
663 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
664 .icon_format = image_format,
665 .index = entry_i,
666 .bitmap_version = bitmap_version,
667 } },
668 });
669 } else if (bitmap_version != .@"nt3.1") {
670 try self.addErrorDetails(.{
671 .err = .rc_would_error_on_bitmap_version,
672 .type = .warning,
673 .token = filename_token,
674 .extra = .{ .icon_dir = .{
675 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
676 .icon_format = image_format,
677 .index = entry_i,
678 .bitmap_version = bitmap_version,
679 } },
680 });
681 }
682
683 switch (icon_dir.image_type) {
684 .icon => {
685 // The values in the icon's BITMAPINFOHEADER always take precedence over
686 // the values in the IconDir, but not in the LOCALHEADER (see above).
687 entry.type_specific_data.icon.color_planes = std.mem.littleToNative(u16, bitmap_header.bcPlanes);
688 entry.type_specific_data.icon.bits_per_pixel = std.mem.littleToNative(u16, bitmap_header.bcBitCount);
689 },
690 .cursor => {
691 // Only cursors get the width/height from BITMAPINFOHEADER (icons don't)
692 entry.width = @intCast(bitmap_header.bcWidth);
693 entry.height = @intCast(bitmap_header.bcHeight);
694 entry.type_specific_data.cursor.hotspot_x = std.mem.littleToNative(u16, bitmap_header.bcPlanes);
695 entry.type_specific_data.cursor.hotspot_y = std.mem.littleToNative(u16, bitmap_header.bcBitCount);
696 },
697 }
698 },
699 }
700
701 try file.seekTo(entry.data_offset_from_start_of_file);
702 try writeResourceDataNoPadding(writer, file.reader(), entry.data_size_in_bytes);
703 try writeDataPadding(writer, full_data_size);
704
705 if (self.state.icon_id == std.math.maxInt(u16)) {
706 try self.addErrorDetails(.{
707 .err = .max_icon_ids_exhausted,
708 .print_source_line = false,
709 .token = filename_token,
710 .extra = .{ .icon_dir = .{
711 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
712 .icon_format = image_format,
713 .index = entry_i,
714 } },
715 });
716 return self.addErrorDetailsAndFail(.{
717 .err = .max_icon_ids_exhausted,
718 .type = .note,
719 .token = filename_token,
720 .extra = .{ .icon_dir = .{
721 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
722 .icon_format = image_format,
723 .index = entry_i,
724 } },
725 });
726 }
727 self.state.icon_id += 1;
728 }
729
730 header.data_size = icon_dir.getResDataSize();
731
732 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
733 try icon_dir.writeResData(writer, first_icon_id);
734 try writeDataPadding(writer, header.data_size);
735 return;
736 },
737 .RCDATA, .HTML, .MANIFEST, .MESSAGETABLE, .DLGINIT, .PLUGPLAY => {
738 header.applyMemoryFlags(node.common_resource_attributes, self.source);
739 },
740 .BITMAP => {
741 header.applyMemoryFlags(node.common_resource_attributes, self.source);
742 const file_size = try file.getEndPos();
743
744 const bitmap_info = bmp.read(file.reader(), file_size) catch |err| {
745 const filename_string_index = try self.diagnostics.putString(filename_utf8);
746 return self.addErrorDetailsAndFail(.{
747 .err = .bmp_read_error,
748 .token = filename_token,
749 .extra = .{ .bmp_read_error = .{
750 .err = ErrorDetails.BitmapReadError.enumFromError(err),
751 .filename_string_index = filename_string_index,
752 } },
753 });
754 };
755
756 if (bitmap_info.getActualPaletteByteLen() > bitmap_info.getExpectedPaletteByteLen()) {
757 const num_ignored_bytes = bitmap_info.getActualPaletteByteLen() - bitmap_info.getExpectedPaletteByteLen();
758 var number_as_bytes: [8]u8 = undefined;
759 std.mem.writeIntNative(u64, &number_as_bytes, num_ignored_bytes);
760 const value_string_index = try self.diagnostics.putString(&number_as_bytes);
761 try self.addErrorDetails(.{
762 .err = .bmp_ignored_palette_bytes,
763 .type = .warning,
764 .token = filename_token,
765 .extra = .{ .number = value_string_index },
766 });
767 } else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {
768 const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();
769
770 // TODO: Make this configurable (command line option)
771 const max_missing_bytes = 4096;
772 if (num_padding_bytes > max_missing_bytes) {
773 var numbers_as_bytes: [16]u8 = undefined;
774 std.mem.writeIntNative(u64, numbers_as_bytes[0..8], num_padding_bytes);
775 std.mem.writeIntNative(u64, numbers_as_bytes[8..16], max_missing_bytes);
776 const values_string_index = try self.diagnostics.putString(&numbers_as_bytes);
777 try self.addErrorDetails(.{
778 .err = .bmp_too_many_missing_palette_bytes,
779 .token = filename_token,
780 .extra = .{ .number = values_string_index },
781 });
782 return self.addErrorDetailsAndFail(.{
783 .err = .bmp_too_many_missing_palette_bytes,
784 .type = .note,
785 .print_source_line = false,
786 .token = filename_token,
787 });
788 }
789
790 var number_as_bytes: [8]u8 = undefined;
791 std.mem.writeIntNative(u64, &number_as_bytes, num_padding_bytes);
792 const value_string_index = try self.diagnostics.putString(&number_as_bytes);
793 try self.addErrorDetails(.{
794 .err = .bmp_missing_palette_bytes,
795 .type = .warning,
796 .token = filename_token,
797 .extra = .{ .number = value_string_index },
798 });
799 const pixel_data_len = bitmap_info.getPixelDataLen(file_size);
800 if (pixel_data_len > 0) {
801 const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);
802 std.mem.writeIntNative(u64, &number_as_bytes, miscompiled_bytes);
803 const miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);
804 try self.addErrorDetails(.{
805 .err = .rc_would_miscompile_bmp_palette_padding,
806 .type = .warning,
807 .token = filename_token,
808 .extra = .{ .number = miscompiled_bytes_string_index },
809 });
810 }
811 }
812
813 // TODO: It might be possible that the calculation done in this function
814 // could underflow if the underlying file is modified while reading
815 // it, but need to think about it more to determine if that's a
816 // real possibility
817 const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));
818
819 header.data_size = bmp_bytes_to_write;
820 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
821 try file.seekTo(bmp.file_header_len);
822 const file_reader = file.reader();
823 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.dib_header_size);
824 if (bitmap_info.getBitmasksByteLen() > 0) {
825 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.getBitmasksByteLen());
826 }
827 if (bitmap_info.getExpectedPaletteByteLen() > 0) {
828 try writeResourceDataNoPadding(writer, file_reader, @intCast(bitmap_info.getActualPaletteByteLen()));
829 const padding_bytes = bitmap_info.getMissingPaletteByteLen();
830 if (padding_bytes > 0) {
831 try writer.writeByteNTimes(0, padding_bytes);
832 }
833 }
834 try file.seekTo(bitmap_info.pixel_data_offset);
835 const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);
836 try writeResourceDataNoPadding(writer, file_reader, pixel_bytes);
837 try writeDataPadding(writer, bmp_bytes_to_write);
838 return;
839 },
840 .FONT => {
841 if (self.state.font_dir.ids.get(header.name_value.ordinal) != null) {
842 // Add warning and skip this resource
843 // Note: The Win32 compiler prints this as an error but it doesn't fail the compilation
844 // and the duplicate resource is skipped.
845 try self.addErrorDetails(ErrorDetails{
846 .err = .font_id_already_defined,
847 .token = node.id,
848 .type = .warning,
849 .extra = .{ .number = header.name_value.ordinal },
850 });
851 try self.addErrorDetails(ErrorDetails{
852 .err = .font_id_already_defined,
853 .token = self.state.font_dir.ids.get(header.name_value.ordinal).?,
854 .type = .note,
855 .extra = .{ .number = header.name_value.ordinal },
856 });
857 return;
858 }
859 header.applyMemoryFlags(node.common_resource_attributes, self.source);
860 const file_size = try file.getEndPos();
861 if (file_size > std.math.maxInt(u32)) {
862 return self.addErrorDetailsAndFail(.{
863 .err = .resource_data_size_exceeds_max,
864 .token = node.id,
865 });
866 }
867
868 // We now know that the data size will fit in a u32
869 header.data_size = @intCast(file_size);
870 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
871
872 var header_slurping_reader = headerSlurpingReader(148, file.reader());
873 try writeResourceData(writer, header_slurping_reader.reader(), header.data_size);
874
875 try self.state.font_dir.add(self.arena, FontDir.Font{
876 .id = header.name_value.ordinal,
877 .header_bytes = header_slurping_reader.slurped_header,
878 }, node.id);
879 return;
880 },
881 .ACCELERATOR,
882 .ANICURSOR,
883 .ANIICON,
884 .CURSOR,
885 .DIALOG,
886 .DLGINCLUDE,
887 .FONTDIR,
888 .ICON,
889 .MENU,
890 .STRING,
891 .TOOLBAR,
892 .VERSION,
893 .VXD,
894 => unreachable,
895 _ => unreachable,
896 }
897 } else {
898 header.applyMemoryFlags(node.common_resource_attributes, self.source);
899 }
900
901 // Fallback to just writing out the entire contents of the file
902 const data_size = try file.getEndPos();
903 if (data_size > std.math.maxInt(u32)) {
904 return self.addErrorDetailsAndFail(.{
905 .err = .resource_data_size_exceeds_max,
906 .token = node.id,
907 });
908 }
909 // We now know that the data size will fit in a u32
910 header.data_size = @intCast(data_size);
911 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
912 try writeResourceData(writer, file.reader(), header.data_size);
913 }
914
915 fn iconReadError(
916 self: *Compiler,
917 err: ico.ReadError,
918 filename: []const u8,
919 token: Token,
920 predefined_type: res.RT,
921 ) error{ CompileError, OutOfMemory } {
922 const filename_string_index = try self.diagnostics.putString(filename);
923 return self.addErrorDetailsAndFail(.{
924 .err = .icon_read_error,
925 .token = token,
926 .extra = .{ .icon_read_error = .{
927 .err = ErrorDetails.IconReadError.enumFromError(err),
928 .icon_type = switch (predefined_type) {
929 .GROUP_ICON => .icon,
930 .GROUP_CURSOR => .cursor,
931 else => unreachable,
932 },
933 .filename_string_index = filename_string_index,
934 } },
935 });
936 }
937
938 pub const DataType = enum {
939 number,
940 ascii_string,
941 wide_string,
942 };
943
944 pub const Data = union(DataType) {
945 number: Number,
946 ascii_string: []const u8,
947 wide_string: [:0]const u16,
948
949 pub fn deinit(self: Data, allocator: Allocator) void {
950 switch (self) {
951 .wide_string => |wide_string| {
952 allocator.free(wide_string);
953 },
954 .ascii_string => |ascii_string| {
955 allocator.free(ascii_string);
956 },
957 else => {},
958 }
959 }
960
961 pub fn write(self: Data, writer: anytype) !void {
962 switch (self) {
963 .number => |number| switch (number.is_long) {
964 false => try writer.writeIntLittle(WORD, number.asWord()),
965 true => try writer.writeIntLittle(DWORD, number.value),
966 },
967 .ascii_string => |ascii_string| {
968 try writer.writeAll(ascii_string);
969 },
970 .wide_string => |wide_string| {
971 try writer.writeAll(std.mem.sliceAsBytes(wide_string));
972 },
973 }
974 }
975 };
976
977 /// Assumes that the node is a number or number expression
978 pub fn evaluateNumberExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) Number {
979 switch (expression_node.id) {
980 .literal => {
981 const literal_node = expression_node.cast(.literal).?;
982 std.debug.assert(literal_node.token.id == .number);
983 const bytes = SourceBytes{
984 .slice = literal_node.token.slice(source),
985 .code_page = code_page_lookup.getForToken(literal_node.token),
986 };
987 return literals.parseNumberLiteral(bytes);
988 },
989 .binary_expression => {
990 const binary_expression_node = expression_node.cast(.binary_expression).?;
991 const lhs = evaluateNumberExpression(binary_expression_node.left, source, code_page_lookup);
992 const rhs = evaluateNumberExpression(binary_expression_node.right, source, code_page_lookup);
993 const operator_char = binary_expression_node.operator.slice(source)[0];
994 return lhs.evaluateOperator(operator_char, rhs);
995 },
996 .grouped_expression => {
997 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
998 return evaluateNumberExpression(grouped_expression_node.expression, source, code_page_lookup);
999 },
1000 else => unreachable,
1001 }
1002 }
1003
1004 const FlagsNumber = struct {
1005 value: u32,
1006 not_mask: u32 = 0xFFFFFFFF,
1007
1008 pub fn evaluateOperator(lhs: FlagsNumber, operator_char: u8, rhs: FlagsNumber) FlagsNumber {
1009 const result = switch (operator_char) {
1010 '-' => lhs.value -% rhs.value,
1011 '+' => lhs.value +% rhs.value,
1012 '|' => lhs.value | rhs.value,
1013 '&' => lhs.value & rhs.value,
1014 else => unreachable, // invalid operator, this would be a lexer/parser bug
1015 };
1016 return .{
1017 .value = result,
1018 .not_mask = lhs.not_mask & rhs.not_mask,
1019 };
1020 }
1021
1022 pub fn applyNotMask(self: FlagsNumber) u32 {
1023 return self.value & self.not_mask;
1024 }
1025 };
1026
1027 pub fn evaluateFlagsExpressionWithDefault(default: u32, expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) u32 {
1028 var context = FlagsExpressionContext{ .initial_value = default };
1029 const number = evaluateFlagsExpression(expression_node, source, code_page_lookup, &context);
1030 return number.value;
1031 }
1032
1033 pub const FlagsExpressionContext = struct {
1034 initial_value: u32 = 0,
1035 initial_value_used: bool = false,
1036 };
1037
1038 /// Assumes that the node is a number expression (which can contain not_expressions)
1039 pub fn evaluateFlagsExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup, context: *FlagsExpressionContext) FlagsNumber {
1040 switch (expression_node.id) {
1041 .literal => {
1042 const literal_node = expression_node.cast(.literal).?;
1043 std.debug.assert(literal_node.token.id == .number);
1044 const bytes = SourceBytes{
1045 .slice = literal_node.token.slice(source),
1046 .code_page = code_page_lookup.getForToken(literal_node.token),
1047 };
1048 var value = literals.parseNumberLiteral(bytes).value;
1049 if (!context.initial_value_used) {
1050 context.initial_value_used = true;
1051 value |= context.initial_value;
1052 }
1053 return .{ .value = value };
1054 },
1055 .binary_expression => {
1056 const binary_expression_node = expression_node.cast(.binary_expression).?;
1057 const lhs = evaluateFlagsExpression(binary_expression_node.left, source, code_page_lookup, context);
1058 const rhs = evaluateFlagsExpression(binary_expression_node.right, source, code_page_lookup, context);
1059 const operator_char = binary_expression_node.operator.slice(source)[0];
1060 const result = lhs.evaluateOperator(operator_char, rhs);
1061 return .{ .value = result.applyNotMask() };
1062 },
1063 .grouped_expression => {
1064 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
1065 return evaluateFlagsExpression(grouped_expression_node.expression, source, code_page_lookup, context);
1066 },
1067 .not_expression => {
1068 const not_expression = expression_node.cast(.not_expression).?;
1069 const bytes = SourceBytes{
1070 .slice = not_expression.number_token.slice(source),
1071 .code_page = code_page_lookup.getForToken(not_expression.number_token),
1072 };
1073 const not_number = literals.parseNumberLiteral(bytes);
1074 if (!context.initial_value_used) {
1075 context.initial_value_used = true;
1076 return .{ .value = context.initial_value & ~not_number.value };
1077 }
1078 return .{ .value = 0, .not_mask = ~not_number.value };
1079 },
1080 else => unreachable,
1081 }
1082 }
1083
1084 pub fn evaluateDataExpression(self: *Compiler, expression_node: *Node) !Data {
1085 switch (expression_node.id) {
1086 .literal => {
1087 const literal_node = expression_node.cast(.literal).?;
1088 switch (literal_node.token.id) {
1089 .number => {
1090 const number = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
1091 return .{ .number = number };
1092 },
1093 .quoted_ascii_string => {
1094 const column = literal_node.token.calculateColumn(self.source, 8, null);
1095 const bytes = SourceBytes{
1096 .slice = literal_node.token.slice(self.source),
1097 .code_page = self.input_code_pages.getForToken(literal_node.token),
1098 };
1099 const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{
1100 .start_column = column,
1101 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
1102 .output_code_page = self.output_code_pages.getForToken(literal_node.token),
1103 });
1104 errdefer self.allocator.free(parsed);
1105 return .{ .ascii_string = parsed };
1106 },
1107 .quoted_wide_string => {
1108 const column = literal_node.token.calculateColumn(self.source, 8, null);
1109 const bytes = SourceBytes{
1110 .slice = literal_node.token.slice(self.source),
1111 .code_page = self.input_code_pages.getForToken(literal_node.token),
1112 };
1113 const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{
1114 .start_column = column,
1115 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
1116 });
1117 errdefer self.allocator.free(parsed_string);
1118 return .{ .wide_string = parsed_string };
1119 },
1120 else => {
1121 std.debug.print("unexpected token in literal node: {}\n", .{literal_node.token});
1122 unreachable; // no other token types should be in a data literal node
1123 },
1124 }
1125 },
1126 .binary_expression, .grouped_expression => {
1127 const result = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
1128 return .{ .number = result };
1129 },
1130 .not_expression => unreachable,
1131 else => {
1132 std.debug.print("{}\n", .{expression_node.id});
1133 @panic("TODO: evaluateDataExpression");
1134 },
1135 }
1136 }
1137
1138 pub fn writeResourceRawData(self: *Compiler, node: *Node.ResourceRawData, writer: anytype) !void {
1139 var data_buffer = std.ArrayList(u8).init(self.allocator);
1140 defer data_buffer.deinit();
1141 // The header's data length field is a u32 so limit the resource's data size so that
1142 // we know we can always specify the real size.
1143 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1144 const data_writer = limited_writer.writer();
1145
1146 for (node.raw_data) |expression| {
1147 const data = try self.evaluateDataExpression(expression);
1148 defer data.deinit(self.allocator);
1149 data.write(data_writer) catch |err| switch (err) {
1150 error.NoSpaceLeft => {
1151 return self.addErrorDetailsAndFail(.{
1152 .err = .resource_data_size_exceeds_max,
1153 .token = node.id,
1154 });
1155 },
1156 else => |e| return e,
1157 };
1158 }
1159
1160 // This intCast can't fail because the limitedWriter above guarantees that
1161 // we will never write more than maxInt(u32) bytes.
1162 const data_len: u32 = @intCast(data_buffer.items.len);
1163 try self.writeResourceHeader(writer, node.id, node.type, data_len, node.common_resource_attributes, self.state.language);
1164
1165 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1166 try writeResourceData(writer, data_fbs.reader(), data_len);
1167 }
1168
1169 pub fn writeResourceHeader(self: *Compiler, writer: anytype, id_token: Token, type_token: Token, data_size: u32, common_resource_attributes: []Token, language: res.Language) !void {
1170 var header = try self.resourceHeader(id_token, type_token, .{
1171 .language = language,
1172 .data_size = data_size,
1173 });
1174 defer header.deinit(self.allocator);
1175
1176 header.applyMemoryFlags(common_resource_attributes, self.source);
1177
1178 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = id_token });
1179 }
1180
1181 pub fn writeResourceDataNoPadding(writer: anytype, data_reader: anytype, data_size: u32) !void {
1182 var limited_reader = std.io.limitedReader(data_reader, data_size);
1183
1184 const FifoBuffer = std.fifo.LinearFifo(u8, .{ .Static = 4096 });
1185 var fifo = FifoBuffer.init();
1186 try fifo.pump(limited_reader.reader(), writer);
1187 }
1188
1189 pub fn writeResourceData(writer: anytype, data_reader: anytype, data_size: u32) !void {
1190 try writeResourceDataNoPadding(writer, data_reader, data_size);
1191 try writeDataPadding(writer, data_size);
1192 }
1193
1194 pub fn writeDataPadding(writer: anytype, data_size: u32) !void {
1195 try writer.writeByteNTimes(0, numPaddingBytesNeeded(data_size));
1196 }
1197
1198 pub fn numPaddingBytesNeeded(data_size: u32) u2 {
1199 // Result is guaranteed to be between 0 and 3.
1200 return @intCast((4 -% data_size) % 4);
1201 }
1202
1203 pub fn evaluateAcceleratorKeyExpression(self: *Compiler, node: *Node, is_virt: bool) !u16 {
1204 if (node.isNumberExpression()) {
1205 return evaluateNumberExpression(node, self.source, self.input_code_pages).asWord();
1206 } else {
1207 std.debug.assert(node.isStringLiteral());
1208 const literal = @fieldParentPtr(Node.Literal, "base", node);
1209 const bytes = SourceBytes{
1210 .slice = literal.token.slice(self.source),
1211 .code_page = self.input_code_pages.getForToken(literal.token),
1212 };
1213 const column = literal.token.calculateColumn(self.source, 8, null);
1214 return res.parseAcceleratorKeyString(bytes, is_virt, .{
1215 .start_column = column,
1216 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal.token },
1217 });
1218 }
1219 }
1220
1221 pub fn writeAccelerators(self: *Compiler, node: *Node.Accelerators, writer: anytype) !void {
1222 var data_buffer = std.ArrayList(u8).init(self.allocator);
1223 defer data_buffer.deinit();
1224
1225 // The header's data length field is a u32 so limit the resource's data size so that
1226 // we know we can always specify the real size.
1227 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1228 const data_writer = limited_writer.writer();
1229
1230 self.writeAcceleratorsData(node, data_writer) catch |err| switch (err) {
1231 error.NoSpaceLeft => {
1232 return self.addErrorDetailsAndFail(.{
1233 .err = .resource_data_size_exceeds_max,
1234 .token = node.id,
1235 });
1236 },
1237 else => |e| return e,
1238 };
1239
1240 // This intCast can't fail because the limitedWriter above guarantees that
1241 // we will never write more than maxInt(u32) bytes.
1242 const data_size: u32 = @intCast(data_buffer.items.len);
1243 var header = try self.resourceHeader(node.id, node.type, .{
1244 .data_size = data_size,
1245 });
1246 defer header.deinit(self.allocator);
1247
1248 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1249 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
1250
1251 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1252
1253 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1254 try writeResourceData(writer, data_fbs.reader(), data_size);
1255 }
1256
1257 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
1258 /// the writer within this function could return error.NoSpaceLeft
1259 pub fn writeAcceleratorsData(self: *Compiler, node: *Node.Accelerators, data_writer: anytype) !void {
1260 for (node.accelerators, 0..) |accel_node, i| {
1261 const accelerator = @fieldParentPtr(Node.Accelerator, "base", accel_node);
1262 var modifiers = res.AcceleratorModifiers{};
1263 for (accelerator.type_and_options) |type_or_option| {
1264 const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;
1265 modifiers.apply(modifier);
1266 }
1267 if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {
1268 return self.addErrorDetailsAndFail(.{
1269 .err = .accelerator_type_required,
1270 .token = accelerator.event.getFirstToken(),
1271 .token_span_end = accelerator.event.getLastToken(),
1272 });
1273 }
1274 const key = self.evaluateAcceleratorKeyExpression(accelerator.event, modifiers.isSet(.virtkey)) catch |err| switch (err) {
1275 error.OutOfMemory => |e| return e,
1276 else => |e| {
1277 return self.addErrorDetailsAndFail(.{
1278 .err = .invalid_accelerator_key,
1279 .token = accelerator.event.getFirstToken(),
1280 .token_span_end = accelerator.event.getLastToken(),
1281 .extra = .{ .accelerator_error = .{
1282 .err = ErrorDetails.AcceleratorError.enumFromError(e),
1283 } },
1284 });
1285 },
1286 };
1287 const cmd_id = evaluateNumberExpression(accelerator.idvalue, self.source, self.input_code_pages);
1288
1289 if (i == node.accelerators.len - 1) {
1290 modifiers.markLast();
1291 }
1292
1293 try data_writer.writeByte(modifiers.value);
1294 try data_writer.writeByte(0); // padding
1295 try data_writer.writeIntLittle(u16, key);
1296 try data_writer.writeIntLittle(u16, cmd_id.asWord());
1297 try data_writer.writeIntLittle(u16, 0); // padding
1298 }
1299 }
1300
1301 const DialogOptionalStatementValues = struct {
1302 style: u32 = res.WS.SYSMENU | res.WS.BORDER | res.WS.POPUP,
1303 exstyle: u32 = 0,
1304 class: ?NameOrOrdinal = null,
1305 menu: ?NameOrOrdinal = null,
1306 font: ?FontStatementValues = null,
1307 caption: ?Token = null,
1308 };
1309
1310 pub fn writeDialog(self: *Compiler, node: *Node.Dialog, writer: anytype) !void {
1311 var data_buffer = std.ArrayList(u8).init(self.allocator);
1312 defer data_buffer.deinit();
1313 // The header's data length field is a u32 so limit the resource's data size so that
1314 // we know we can always specify the real size.
1315 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1316 const data_writer = limited_writer.writer();
1317
1318 const resource = Resource.fromString(.{
1319 .slice = node.type.slice(self.source),
1320 .code_page = self.input_code_pages.getForToken(node.type),
1321 });
1322 std.debug.assert(resource == .dialog or resource == .dialogex);
1323
1324 var optional_statement_values: DialogOptionalStatementValues = .{};
1325 defer {
1326 if (optional_statement_values.class) |class| {
1327 class.deinit(self.allocator);
1328 }
1329 if (optional_statement_values.menu) |menu| {
1330 menu.deinit(self.allocator);
1331 }
1332 }
1333 var skipped_menu_or_classes = std.ArrayList(*Node.SimpleStatement).init(self.allocator);
1334 defer skipped_menu_or_classes.deinit();
1335 var last_menu: *Node.SimpleStatement = undefined;
1336 var last_class: *Node.SimpleStatement = undefined;
1337 var last_menu_would_be_forced_ordinal = false;
1338 var last_menu_has_digit_as_first_char = false;
1339 var last_menu_did_uppercase = false;
1340 var last_class_would_be_forced_ordinal = false;
1341
1342 for (node.optional_statements) |optional_statement| {
1343 switch (optional_statement.id) {
1344 .simple_statement => {
1345 const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", optional_statement);
1346 const statement_identifier = simple_statement.identifier;
1347 const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
1348 switch (statement_type) {
1349 .style, .exstyle => {
1350 const style = evaluateFlagsExpressionWithDefault(0, simple_statement.value, self.source, self.input_code_pages);
1351 if (statement_type == .style) {
1352 optional_statement_values.style = style;
1353 } else {
1354 optional_statement_values.exstyle = style;
1355 }
1356 },
1357 .caption => {
1358 std.debug.assert(simple_statement.value.id == .literal);
1359 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1360 optional_statement_values.caption = literal_node.token;
1361 },
1362 .class => {
1363 const is_duplicate = optional_statement_values.class != null;
1364 if (is_duplicate) {
1365 try skipped_menu_or_classes.append(last_class);
1366 }
1367 const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;
1368 // In the Win32 RC compiler, if any CLASS values that are interpreted as
1369 // an ordinal exist, it affects all future CLASS statements and forces
1370 // them to be treated as an ordinal no matter what.
1371 if (forced_ordinal) {
1372 last_class_would_be_forced_ordinal = true;
1373 }
1374 // clear out the old one if it exists
1375 if (optional_statement_values.class) |prev| {
1376 prev.deinit(self.allocator);
1377 optional_statement_values.class = null;
1378 }
1379
1380 if (simple_statement.value.isNumberExpression()) {
1381 const class_ordinal = evaluateNumberExpression(simple_statement.value, self.source, self.input_code_pages);
1382 optional_statement_values.class = NameOrOrdinal{ .ordinal = class_ordinal.asWord() };
1383 } else {
1384 std.debug.assert(simple_statement.value.isStringLiteral());
1385 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1386 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
1387 optional_statement_values.class = NameOrOrdinal{ .name = parsed };
1388 }
1389
1390 last_class = simple_statement;
1391 },
1392 .menu => {
1393 const is_duplicate = optional_statement_values.menu != null;
1394 if (is_duplicate) {
1395 try skipped_menu_or_classes.append(last_menu);
1396 }
1397 const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;
1398 // In the Win32 RC compiler, if any MENU values that are interpreted as
1399 // an ordinal exist, it affects all future MENU statements and forces
1400 // them to be treated as an ordinal no matter what.
1401 if (forced_ordinal) {
1402 last_menu_would_be_forced_ordinal = true;
1403 }
1404 // clear out the old one if it exists
1405 if (optional_statement_values.menu) |prev| {
1406 prev.deinit(self.allocator);
1407 optional_statement_values.menu = null;
1408 }
1409
1410 std.debug.assert(simple_statement.value.id == .literal);
1411 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1412
1413 const token_slice = literal_node.token.slice(self.source);
1414 const bytes = SourceBytes{
1415 .slice = token_slice,
1416 .code_page = self.input_code_pages.getForToken(literal_node.token),
1417 };
1418 optional_statement_values.menu = try NameOrOrdinal.fromString(self.allocator, bytes);
1419
1420 if (optional_statement_values.menu.? == .name) {
1421 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(bytes)) |win32_rc_ordinal| {
1422 try self.addErrorDetails(.{
1423 .err = .invalid_digit_character_in_ordinal,
1424 .type = .err,
1425 .token = literal_node.token,
1426 });
1427 return self.addErrorDetailsAndFail(.{
1428 .err = .win32_non_ascii_ordinal,
1429 .type = .note,
1430 .token = literal_node.token,
1431 .print_source_line = false,
1432 .extra = .{ .number = win32_rc_ordinal.ordinal },
1433 });
1434 }
1435 }
1436
1437 // Need to keep track of some properties of the value
1438 // in order to emit the appropriate warning(s) later on.
1439 // See where the warning are emitted below (outside this loop)
1440 // for the full explanation.
1441 var did_uppercase = false;
1442 var codepoint_i: usize = 0;
1443 while (bytes.code_page.codepointAt(codepoint_i, bytes.slice)) |codepoint| : (codepoint_i += codepoint.byte_len) {
1444 const c = codepoint.value;
1445 switch (c) {
1446 'a'...'z' => {
1447 did_uppercase = true;
1448 break;
1449 },
1450 else => {},
1451 }
1452 }
1453 last_menu_did_uppercase = did_uppercase;
1454 last_menu_has_digit_as_first_char = std.ascii.isDigit(token_slice[0]);
1455 last_menu = simple_statement;
1456 },
1457 else => {},
1458 }
1459 },
1460 .font_statement => {
1461 const font = @fieldParentPtr(Node.FontStatement, "base", optional_statement);
1462 if (optional_statement_values.font != null) {
1463 optional_statement_values.font.?.node = font;
1464 } else {
1465 optional_statement_values.font = FontStatementValues{ .node = font };
1466 }
1467 if (font.weight) |weight| {
1468 const value = evaluateNumberExpression(weight, self.source, self.input_code_pages);
1469 optional_statement_values.font.?.weight = value.asWord();
1470 }
1471 if (font.italic) |italic| {
1472 const value = evaluateNumberExpression(italic, self.source, self.input_code_pages);
1473 optional_statement_values.font.?.italic = value.asWord() != 0;
1474 }
1475 },
1476 else => {},
1477 }
1478 }
1479
1480 for (skipped_menu_or_classes.items) |simple_statement| {
1481 const statement_identifier = simple_statement.identifier;
1482 const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
1483 try self.addErrorDetails(.{
1484 .err = .duplicate_menu_or_class_skipped,
1485 .type = .warning,
1486 .token = simple_statement.identifier,
1487 .token_span_start = simple_statement.base.getFirstToken(),
1488 .token_span_end = simple_statement.base.getLastToken(),
1489 .extra = .{ .menu_or_class = switch (statement_type) {
1490 .menu => .menu,
1491 .class => .class,
1492 else => unreachable,
1493 } },
1494 });
1495 }
1496 // The Win32 RC compiler miscompiles the value in the following scenario:
1497 // Multiple CLASS parameters are specified and any of them are treated as a number, then
1498 // the last CLASS is always treated as a number no matter what
1499 if (last_class_would_be_forced_ordinal and optional_statement_values.class.? == .name) {
1500 const literal_node = @fieldParentPtr(Node.Literal, "base", last_class.value);
1501 const ordinal_value = res.ForcedOrdinal.fromUtf16Le(optional_statement_values.class.?.name);
1502
1503 try self.addErrorDetails(.{
1504 .err = .rc_would_miscompile_dialog_class,
1505 .type = .warning,
1506 .token = literal_node.token,
1507 .extra = .{ .number = ordinal_value },
1508 });
1509 try self.addErrorDetails(.{
1510 .err = .rc_would_miscompile_dialog_class,
1511 .type = .note,
1512 .print_source_line = false,
1513 .token = literal_node.token,
1514 .extra = .{ .number = ordinal_value },
1515 });
1516 try self.addErrorDetails(.{
1517 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
1518 .type = .note,
1519 .print_source_line = false,
1520 .token = literal_node.token,
1521 .extra = .{ .menu_or_class = .class },
1522 });
1523 }
1524 // The Win32 RC compiler miscompiles the id in two different scenarios:
1525 // 1. The first character of the ID is a digit, in which case it is always treated as a number
1526 // no matter what (and therefore does not match how the MENU/MENUEX id is parsed)
1527 // 2. Multiple MENU parameters are specified and any of them are treated as a number, then
1528 // the last MENU is always treated as a number no matter what
1529 if ((last_menu_would_be_forced_ordinal or last_menu_has_digit_as_first_char) and optional_statement_values.menu.? == .name) {
1530 const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
1531 const token_slice = literal_node.token.slice(self.source);
1532 const bytes = SourceBytes{
1533 .slice = token_slice,
1534 .code_page = self.input_code_pages.getForToken(literal_node.token),
1535 };
1536 const ordinal_value = res.ForcedOrdinal.fromBytes(bytes);
1537
1538 try self.addErrorDetails(.{
1539 .err = .rc_would_miscompile_dialog_menu_id,
1540 .type = .warning,
1541 .token = literal_node.token,
1542 .extra = .{ .number = ordinal_value },
1543 });
1544 try self.addErrorDetails(.{
1545 .err = .rc_would_miscompile_dialog_menu_id,
1546 .type = .note,
1547 .print_source_line = false,
1548 .token = literal_node.token,
1549 .extra = .{ .number = ordinal_value },
1550 });
1551 if (last_menu_would_be_forced_ordinal) {
1552 try self.addErrorDetails(.{
1553 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
1554 .type = .note,
1555 .print_source_line = false,
1556 .token = literal_node.token,
1557 .extra = .{ .menu_or_class = .menu },
1558 });
1559 } else {
1560 try self.addErrorDetails(.{
1561 .err = .rc_would_miscompile_dialog_menu_id_starts_with_digit,
1562 .type = .note,
1563 .print_source_line = false,
1564 .token = literal_node.token,
1565 });
1566 }
1567 }
1568 // The MENU id parsing uses the exact same logic as the MENU/MENUEX resource id parsing,
1569 // which means that it will convert ASCII characters to uppercase during the 'name' parsing.
1570 // This turns out not to matter (`LoadMenu` does a case-insensitive lookup anyway),
1571 // but it still makes sense to share the uppercasing logic since the MENU parameter
1572 // here is just a reference to a MENU/MENUEX id within the .exe.
1573 // So, because this is an intentional but inconsequential-to-the-user difference
1574 // between resinator and the Win32 RC compiler, we only emit a hint instead of
1575 // a warning.
1576 if (last_menu_did_uppercase) {
1577 const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
1578 try self.addErrorDetails(.{
1579 .err = .dialog_menu_id_was_uppercased,
1580 .type = .hint,
1581 .token = literal_node.token,
1582 });
1583 }
1584
1585 const x = evaluateNumberExpression(node.x, self.source, self.input_code_pages);
1586 const y = evaluateNumberExpression(node.y, self.source, self.input_code_pages);
1587 const width = evaluateNumberExpression(node.width, self.source, self.input_code_pages);
1588 const height = evaluateNumberExpression(node.height, self.source, self.input_code_pages);
1589
1590 // FONT statement requires DS_SETFONT, and if it's not present DS_SETFRONT must be unset
1591 if (optional_statement_values.font) |_| {
1592 optional_statement_values.style |= res.DS.SETFONT;
1593 } else {
1594 optional_statement_values.style &= ~res.DS.SETFONT;
1595 }
1596 // CAPTION statement implies WS_CAPTION
1597 if (optional_statement_values.caption) |_| {
1598 optional_statement_values.style |= res.WS.CAPTION;
1599 }
1600
1601 self.writeDialogHeaderAndStrings(
1602 node,
1603 data_writer,
1604 resource,
1605 &optional_statement_values,
1606 x,
1607 y,
1608 width,
1609 height,
1610 ) catch |err| switch (err) {
1611 // Dialog header and menu/class/title strings can never exceed u32 bytes
1612 // on their own, so this error is unreachable.
1613 error.NoSpaceLeft => unreachable,
1614 else => |e| return e,
1615 };
1616
1617 var controls_by_id = std.AutoHashMap(u32, *const Node.ControlStatement).init(self.allocator);
1618 // Number of controls are guaranteed by the parser to be within maxInt(u16).
1619 try controls_by_id.ensureTotalCapacity(@as(u16, @intCast(node.controls.len)));
1620 defer controls_by_id.deinit();
1621
1622 for (node.controls) |control_node| {
1623 const control = @fieldParentPtr(Node.ControlStatement, "base", control_node);
1624
1625 self.writeDialogControl(
1626 control,
1627 data_writer,
1628 resource,
1629 // We know the data_buffer len is limited to u32 max.
1630 @intCast(data_buffer.items.len),
1631 &controls_by_id,
1632 ) catch |err| switch (err) {
1633 error.NoSpaceLeft => {
1634 try self.addErrorDetails(.{
1635 .err = .resource_data_size_exceeds_max,
1636 .token = node.id,
1637 });
1638 return self.addErrorDetailsAndFail(.{
1639 .err = .resource_data_size_exceeds_max,
1640 .type = .note,
1641 .token = control.type,
1642 });
1643 },
1644 else => |e| return e,
1645 };
1646 }
1647
1648 const data_size: u32 = @intCast(data_buffer.items.len);
1649 var header = try self.resourceHeader(node.id, node.type, .{
1650 .data_size = data_size,
1651 });
1652 defer header.deinit(self.allocator);
1653
1654 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1655 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
1656
1657 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1658
1659 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1660 try writeResourceData(writer, data_fbs.reader(), data_size);
1661 }
1662
1663 fn writeDialogHeaderAndStrings(
1664 self: *Compiler,
1665 node: *Node.Dialog,
1666 data_writer: anytype,
1667 resource: Resource,
1668 optional_statement_values: *const DialogOptionalStatementValues,
1669 x: Number,
1670 y: Number,
1671 width: Number,
1672 height: Number,
1673 ) !void {
1674 // Header
1675 if (resource == .dialogex) {
1676 const help_id: u32 = help_id: {
1677 if (node.help_id == null) break :help_id 0;
1678 break :help_id evaluateNumberExpression(node.help_id.?, self.source, self.input_code_pages).value;
1679 };
1680 try data_writer.writeIntLittle(u16, 1); // version number, always 1
1681 try data_writer.writeIntLittle(u16, 0xFFFF); // signature, always 0xFFFF
1682 try data_writer.writeIntLittle(u32, help_id);
1683 try data_writer.writeIntLittle(u32, optional_statement_values.exstyle);
1684 try data_writer.writeIntLittle(u32, optional_statement_values.style);
1685 } else {
1686 try data_writer.writeIntLittle(u32, optional_statement_values.style);
1687 try data_writer.writeIntLittle(u32, optional_statement_values.exstyle);
1688 }
1689 // This limit is enforced by the parser, so we know the number of controls
1690 // is within the range of a u16.
1691 try data_writer.writeIntLittle(u16, @as(u16, @intCast(node.controls.len)));
1692 try data_writer.writeIntLittle(u16, x.asWord());
1693 try data_writer.writeIntLittle(u16, y.asWord());
1694 try data_writer.writeIntLittle(u16, width.asWord());
1695 try data_writer.writeIntLittle(u16, height.asWord());
1696
1697 // Menu
1698 if (optional_statement_values.menu) |menu| {
1699 try menu.write(data_writer);
1700 } else {
1701 try data_writer.writeIntLittle(u16, 0);
1702 }
1703 // Class
1704 if (optional_statement_values.class) |class| {
1705 try class.write(data_writer);
1706 } else {
1707 try data_writer.writeIntLittle(u16, 0);
1708 }
1709 // Caption
1710 if (optional_statement_values.caption) |caption| {
1711 const parsed = try self.parseQuotedStringAsWideString(caption);
1712 defer self.allocator.free(parsed);
1713 try data_writer.writeAll(std.mem.sliceAsBytes(parsed[0 .. parsed.len + 1]));
1714 } else {
1715 try data_writer.writeIntLittle(u16, 0);
1716 }
1717 // Font
1718 if (optional_statement_values.font) |font| {
1719 try self.writeDialogFont(resource, font, data_writer);
1720 }
1721 }
1722
1723 fn writeDialogControl(
1724 self: *Compiler,
1725 control: *Node.ControlStatement,
1726 data_writer: anytype,
1727 resource: Resource,
1728 bytes_written_so_far: u32,
1729 controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),
1730 ) !void {
1731 const control_type = rc.Control.map.get(control.type.slice(self.source)).?;
1732
1733 // Each control must be at a 4-byte boundary. However, the Windows RC
1734 // compiler will miscompile controls if their extra data ends on an odd offset.
1735 // We will avoid the miscompilation and emit a warning.
1736 const num_padding = numPaddingBytesNeeded(bytes_written_so_far);
1737 if (num_padding == 1 or num_padding == 3) {
1738 try self.addErrorDetails(.{
1739 .err = .rc_would_miscompile_control_padding,
1740 .type = .warning,
1741 .token = control.type,
1742 });
1743 try self.addErrorDetails(.{
1744 .err = .rc_would_miscompile_control_padding,
1745 .type = .note,
1746 .print_source_line = false,
1747 .token = control.type,
1748 });
1749 }
1750 try data_writer.writeByteNTimes(0, num_padding);
1751
1752 var style = if (control.style) |style_expression|
1753 // Certain styles are implied by the control type
1754 evaluateFlagsExpressionWithDefault(res.ControlClass.getImpliedStyle(control_type), style_expression, self.source, self.input_code_pages)
1755 else
1756 res.ControlClass.getImpliedStyle(control_type);
1757
1758 var exstyle = if (control.exstyle) |exstyle_expression|
1759 evaluateFlagsExpressionWithDefault(0, exstyle_expression, self.source, self.input_code_pages)
1760 else
1761 0;
1762
1763 switch (resource) {
1764 .dialog => {
1765 // Note: Reverse order from DIALOGEX
1766 try data_writer.writeIntLittle(u32, style);
1767 try data_writer.writeIntLittle(u32, exstyle);
1768 },
1769 .dialogex => {
1770 const help_id: u32 = if (control.help_id) |help_id_expression|
1771 evaluateNumberExpression(help_id_expression, self.source, self.input_code_pages).value
1772 else
1773 0;
1774 try data_writer.writeIntLittle(u32, help_id);
1775 // Note: Reverse order from DIALOG
1776 try data_writer.writeIntLittle(u32, exstyle);
1777 try data_writer.writeIntLittle(u32, style);
1778 },
1779 else => unreachable,
1780 }
1781
1782 const control_x = evaluateNumberExpression(control.x, self.source, self.input_code_pages);
1783 const control_y = evaluateNumberExpression(control.y, self.source, self.input_code_pages);
1784 const control_width = evaluateNumberExpression(control.width, self.source, self.input_code_pages);
1785 const control_height = evaluateNumberExpression(control.height, self.source, self.input_code_pages);
1786
1787 try data_writer.writeIntLittle(u16, control_x.asWord());
1788 try data_writer.writeIntLittle(u16, control_y.asWord());
1789 try data_writer.writeIntLittle(u16, control_width.asWord());
1790 try data_writer.writeIntLittle(u16, control_height.asWord());
1791
1792 const control_id = evaluateNumberExpression(control.id, self.source, self.input_code_pages);
1793 switch (resource) {
1794 .dialog => try data_writer.writeIntLittle(u16, control_id.asWord()),
1795 .dialogex => try data_writer.writeIntLittle(u32, control_id.value),
1796 else => unreachable,
1797 }
1798
1799 const control_id_for_map: u32 = switch (resource) {
1800 .dialog => control_id.asWord(),
1801 .dialogex => control_id.value,
1802 else => unreachable,
1803 };
1804 const result = controls_by_id.getOrPutAssumeCapacity(control_id_for_map);
1805 if (result.found_existing) {
1806 if (!self.silent_duplicate_control_ids) {
1807 try self.addErrorDetails(.{
1808 .err = .control_id_already_defined,
1809 .type = .warning,
1810 .token = control.id.getFirstToken(),
1811 .token_span_end = control.id.getLastToken(),
1812 .extra = .{ .number = control_id_for_map },
1813 });
1814 try self.addErrorDetails(.{
1815 .err = .control_id_already_defined,
1816 .type = .note,
1817 .token = result.value_ptr.*.id.getFirstToken(),
1818 .token_span_end = result.value_ptr.*.id.getLastToken(),
1819 .extra = .{ .number = control_id_for_map },
1820 });
1821 }
1822 } else {
1823 result.value_ptr.* = control;
1824 }
1825
1826 if (res.ControlClass.fromControl(control_type)) |control_class| {
1827 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1828 try ordinal.write(data_writer);
1829 } else {
1830 const class_node = control.class.?;
1831 if (class_node.isNumberExpression()) {
1832 const number = evaluateNumberExpression(class_node, self.source, self.input_code_pages);
1833 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
1834 // This is different from how the Windows RC compiles ordinals here,
1835 // but I think that's a miscompilation/bug of the Windows implementation.
1836 // The Windows behavior is (where LSB = least significant byte):
1837 // - If the LSB is 0x00 => 0xFFFF0000
1838 // - If the LSB is < 0x80 => 0x000000<LSB>
1839 // - If the LSB is >= 0x80 => 0x0000FF<LSB>
1840 //
1841 // Because of this, we emit a warning about the potential miscompilation
1842 try self.addErrorDetails(.{
1843 .err = .rc_would_miscompile_control_class_ordinal,
1844 .type = .warning,
1845 .token = class_node.getFirstToken(),
1846 .token_span_end = class_node.getLastToken(),
1847 });
1848 try self.addErrorDetails(.{
1849 .err = .rc_would_miscompile_control_class_ordinal,
1850 .type = .note,
1851 .print_source_line = false,
1852 .token = class_node.getFirstToken(),
1853 .token_span_end = class_node.getLastToken(),
1854 });
1855 // And then write out the ordinal using a proper a NameOrOrdinal encoding.
1856 try ordinal.write(data_writer);
1857 } else if (class_node.isStringLiteral()) {
1858 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
1859 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
1860 defer self.allocator.free(parsed);
1861 if (rc.ControlClass.fromWideString(parsed)) |control_class| {
1862 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1863 try ordinal.write(data_writer);
1864 } else {
1865 // NUL acts as a terminator
1866 // TODO: Maybe warn when parsed_terminated.len != parsed.len, since
1867 // it seems unlikely that NUL-termination is something intentional
1868 const parsed_terminated = std.mem.sliceTo(parsed, 0);
1869 const name = NameOrOrdinal{ .name = parsed_terminated };
1870 try name.write(data_writer);
1871 }
1872 } else {
1873 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
1874 const literal_slice = literal_node.token.slice(self.source);
1875 // This succeeding is guaranteed by the parser
1876 const control_class = rc.ControlClass.map.get(literal_slice) orelse unreachable;
1877 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1878 try ordinal.write(data_writer);
1879 }
1880 }
1881
1882 if (control.text) |text_token| {
1883 const bytes = SourceBytes{
1884 .slice = text_token.slice(self.source),
1885 .code_page = self.input_code_pages.getForToken(text_token),
1886 };
1887 if (text_token.isStringLiteral()) {
1888 const text = try self.parseQuotedStringAsWideString(text_token);
1889 defer self.allocator.free(text);
1890 const name = NameOrOrdinal{ .name = text };
1891 try name.write(data_writer);
1892 } else {
1893 std.debug.assert(text_token.id == .number);
1894 const number = literals.parseNumberLiteral(bytes);
1895 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
1896 try ordinal.write(data_writer);
1897 }
1898 } else {
1899 try NameOrOrdinal.writeEmpty(data_writer);
1900 }
1901
1902 var extra_data_buf = std.ArrayList(u8).init(self.allocator);
1903 defer extra_data_buf.deinit();
1904 // The extra data byte length must be able to fit within a u16.
1905 var limited_extra_data_writer = limitedWriter(extra_data_buf.writer(), std.math.maxInt(u16));
1906 const extra_data_writer = limited_extra_data_writer.writer();
1907 for (control.extra_data) |data_expression| {
1908 const data = try self.evaluateDataExpression(data_expression);
1909 defer data.deinit(self.allocator);
1910 data.write(extra_data_writer) catch |err| switch (err) {
1911 error.NoSpaceLeft => {
1912 try self.addErrorDetails(.{
1913 .err = .control_extra_data_size_exceeds_max,
1914 .token = control.type,
1915 });
1916 return self.addErrorDetailsAndFail(.{
1917 .err = .control_extra_data_size_exceeds_max,
1918 .type = .note,
1919 .token = data_expression.getFirstToken(),
1920 .token_span_end = data_expression.getLastToken(),
1921 });
1922 },
1923 else => |e| return e,
1924 };
1925 }
1926 // We know the extra_data_buf size fits within a u16.
1927 const extra_data_size: u16 = @intCast(extra_data_buf.items.len);
1928 try data_writer.writeIntLittle(u16, extra_data_size);
1929 try data_writer.writeAll(extra_data_buf.items);
1930 }
1931
1932 pub fn writeToolbar(self: *Compiler, node: *Node.Toolbar, writer: anytype) !void {
1933 var data_buffer = std.ArrayList(u8).init(self.allocator);
1934 defer data_buffer.deinit();
1935 const data_writer = data_buffer.writer();
1936
1937 const button_width = evaluateNumberExpression(node.button_width, self.source, self.input_code_pages);
1938 const button_height = evaluateNumberExpression(node.button_height, self.source, self.input_code_pages);
1939
1940 // I'm assuming this is some sort of version
1941 // TODO: Try to find something mentioning this
1942 try data_writer.writeIntLittle(u16, 1);
1943 try data_writer.writeIntLittle(u16, button_width.asWord());
1944 try data_writer.writeIntLittle(u16, button_height.asWord());
1945 try data_writer.writeIntLittle(u16, @as(u16, @intCast(node.buttons.len)));
1946
1947 for (node.buttons) |button_or_sep| {
1948 switch (button_or_sep.id) {
1949 .literal => { // This is always SEPARATOR
1950 std.debug.assert(button_or_sep.cast(.literal).?.token.id == .literal);
1951 try data_writer.writeIntLittle(u16, 0);
1952 },
1953 .simple_statement => {
1954 const value_node = button_or_sep.cast(.simple_statement).?.value;
1955 const value = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
1956 try data_writer.writeIntLittle(u16, value.asWord());
1957 },
1958 else => unreachable, // This is a bug in the parser
1959 }
1960 }
1961
1962 const data_size: u32 = @intCast(data_buffer.items.len);
1963 var header = try self.resourceHeader(node.id, node.type, .{
1964 .data_size = data_size,
1965 });
1966 defer header.deinit(self.allocator);
1967
1968 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1969
1970 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1971
1972 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1973 try writeResourceData(writer, data_fbs.reader(), data_size);
1974 }
1975
1976 /// Weight and italic carry over from previous FONT statements within a single resource,
1977 /// so they need to be parsed ahead-of-time and stored
1978 const FontStatementValues = struct {
1979 weight: u16 = 0,
1980 italic: bool = false,
1981 node: *Node.FontStatement,
1982 };
1983
1984 pub fn writeDialogFont(self: *Compiler, resource: Resource, values: FontStatementValues, writer: anytype) !void {
1985 const node = values.node;
1986 const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);
1987 try writer.writeIntLittle(u16, point_size.asWord());
1988
1989 if (resource == .dialogex) {
1990 try writer.writeIntLittle(u16, values.weight);
1991 }
1992
1993 if (resource == .dialogex) {
1994 try writer.writeIntLittle(u8, @intFromBool(values.italic));
1995 }
1996
1997 if (node.char_set) |char_set| {
1998 const value = evaluateNumberExpression(char_set, self.source, self.input_code_pages);
1999 try writer.writeIntLittle(u8, @as(u8, @truncate(value.value)));
2000 } else if (resource == .dialogex) {
2001 try writer.writeIntLittle(u8, 1); // DEFAULT_CHARSET
2002 }
2003
2004 const typeface = try self.parseQuotedStringAsWideString(node.typeface);
2005 defer self.allocator.free(typeface);
2006 try writer.writeAll(std.mem.sliceAsBytes(typeface[0 .. typeface.len + 1]));
2007 }
2008
2009 pub fn writeMenu(self: *Compiler, node: *Node.Menu, writer: anytype) !void {
2010 var data_buffer = std.ArrayList(u8).init(self.allocator);
2011 defer data_buffer.deinit();
2012 // The header's data length field is a u32 so limit the resource's data size so that
2013 // we know we can always specify the real size.
2014 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
2015 const data_writer = limited_writer.writer();
2016
2017 const type_bytes = SourceBytes{
2018 .slice = node.type.slice(self.source),
2019 .code_page = self.input_code_pages.getForToken(node.type),
2020 };
2021 const resource = Resource.fromString(type_bytes);
2022 std.debug.assert(resource == .menu or resource == .menuex);
2023
2024 self.writeMenuData(node, data_writer, resource) catch |err| switch (err) {
2025 error.NoSpaceLeft => {
2026 return self.addErrorDetailsAndFail(.{
2027 .err = .resource_data_size_exceeds_max,
2028 .token = node.id,
2029 });
2030 },
2031 else => |e| return e,
2032 };
2033
2034 // This intCast can't fail because the limitedWriter above guarantees that
2035 // we will never write more than maxInt(u32) bytes.
2036 const data_size: u32 = @intCast(data_buffer.items.len);
2037 var header = try self.resourceHeader(node.id, node.type, .{
2038 .data_size = data_size,
2039 });
2040 defer header.deinit(self.allocator);
2041
2042 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2043 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
2044
2045 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
2046
2047 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2048 try writeResourceData(writer, data_fbs.reader(), data_size);
2049 }
2050
2051 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
2052 /// the writer within this function could return error.NoSpaceLeft
2053 pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: anytype, resource: Resource) !void {
2054 // menu header
2055 const version: u16 = if (resource == .menu) 0 else 1;
2056 try data_writer.writeIntLittle(u16, version);
2057 const header_size: u16 = if (resource == .menu) 0 else 4;
2058 try data_writer.writeIntLittle(u16, header_size); // cbHeaderSize
2059 // Note: There can be extra bytes at the end of this header (`rgbExtra`),
2060 // but they are always zero-length for us, so we don't write anything
2061 // (the length of the rgbExtra field is inferred from the header_size).
2062 // MENU => rgbExtra: [cbHeaderSize]u8
2063 // MENUEX => rgbExtra: [cbHeaderSize-4]u8
2064
2065 if (resource == .menuex) {
2066 if (node.help_id) |help_id_node| {
2067 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
2068 try data_writer.writeIntLittle(u32, help_id.value);
2069 } else {
2070 try data_writer.writeIntLittle(u32, 0);
2071 }
2072 }
2073
2074 for (node.items, 0..) |item, i| {
2075 const is_last = i == node.items.len - 1;
2076 try self.writeMenuItem(item, data_writer, is_last);
2077 }
2078 }
2079
2080 pub fn writeMenuItem(self: *Compiler, node: *Node, writer: anytype, is_last_of_parent: bool) !void {
2081 switch (node.id) {
2082 .menu_item_separator => {
2083 // This is the 'alternate compability form' of the separator, see
2084 // https://devblogs.microsoft.com/oldnewthing/20080710-00/?p=21673
2085 //
2086 // The 'correct' way is to set the MF_SEPARATOR flag, but the Win32 RC
2087 // compiler still uses this alternate form, so that's what we use too.
2088 var flags = res.MenuItemFlags{};
2089 if (is_last_of_parent) flags.markLast();
2090 try writer.writeIntLittle(u16, flags.value);
2091 try writer.writeIntLittle(u16, 0); // id
2092 try writer.writeIntLittle(u16, 0); // null-terminated UTF-16 text
2093 },
2094 .menu_item => {
2095 const menu_item = @fieldParentPtr(Node.MenuItem, "base", node);
2096 var flags = res.MenuItemFlags{};
2097 for (menu_item.option_list) |option_token| {
2098 // This failing would be a bug in the parser
2099 const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
2100 flags.apply(option);
2101 }
2102 if (is_last_of_parent) flags.markLast();
2103 try writer.writeIntLittle(u16, flags.value);
2104
2105 var result = evaluateNumberExpression(menu_item.result, self.source, self.input_code_pages);
2106 try writer.writeIntLittle(u16, result.asWord());
2107
2108 var text = try self.parseQuotedStringAsWideString(menu_item.text);
2109 defer self.allocator.free(text);
2110 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2111 },
2112 .popup => {
2113 const popup = @fieldParentPtr(Node.Popup, "base", node);
2114 var flags = res.MenuItemFlags{ .value = res.MF.POPUP };
2115 for (popup.option_list) |option_token| {
2116 // This failing would be a bug in the parser
2117 const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
2118 flags.apply(option);
2119 }
2120 if (is_last_of_parent) flags.markLast();
2121 try writer.writeIntLittle(u16, flags.value);
2122
2123 var text = try self.parseQuotedStringAsWideString(popup.text);
2124 defer self.allocator.free(text);
2125 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2126
2127 for (popup.items, 0..) |item, i| {
2128 const is_last = i == popup.items.len - 1;
2129 try self.writeMenuItem(item, writer, is_last);
2130 }
2131 },
2132 inline .menu_item_ex, .popup_ex => |node_type| {
2133 const menu_item = @fieldParentPtr(node_type.Type(), "base", node);
2134
2135 if (menu_item.type) |flags| {
2136 const value = evaluateNumberExpression(flags, self.source, self.input_code_pages);
2137 try writer.writeIntLittle(u32, value.value);
2138 } else {
2139 try writer.writeIntLittle(u32, 0);
2140 }
2141
2142 if (menu_item.state) |state| {
2143 const value = evaluateNumberExpression(state, self.source, self.input_code_pages);
2144 try writer.writeIntLittle(u32, value.value);
2145 } else {
2146 try writer.writeIntLittle(u32, 0);
2147 }
2148
2149 if (menu_item.id) |id| {
2150 const value = evaluateNumberExpression(id, self.source, self.input_code_pages);
2151 try writer.writeIntLittle(u32, value.value);
2152 } else {
2153 try writer.writeIntLittle(u32, 0);
2154 }
2155
2156 var flags: u16 = 0;
2157 if (is_last_of_parent) flags |= comptime @as(u16, @intCast(res.MF.END));
2158 // This constant doesn't seem to have a named #define, it's different than MF_POPUP
2159 if (node_type == .popup_ex) flags |= 0x01;
2160 try writer.writeIntLittle(u16, flags);
2161
2162 var text = try self.parseQuotedStringAsWideString(menu_item.text);
2163 defer self.allocator.free(text);
2164 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2165
2166 // Only the combination of the flags u16 and the text bytes can cause
2167 // non-DWORD alignment, so we can just use the byte length of those
2168 // two values to realign to DWORD alignment.
2169 const relevant_bytes = 2 + (text.len + 1) * 2;
2170 try writeDataPadding(writer, @intCast(relevant_bytes));
2171
2172 if (node_type == .popup_ex) {
2173 if (menu_item.help_id) |help_id_node| {
2174 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
2175 try writer.writeIntLittle(u32, help_id.value);
2176 } else {
2177 try writer.writeIntLittle(u32, 0);
2178 }
2179
2180 for (menu_item.items, 0..) |item, i| {
2181 const is_last = i == menu_item.items.len - 1;
2182 try self.writeMenuItem(item, writer, is_last);
2183 }
2184 }
2185 },
2186 else => unreachable,
2187 }
2188 }
2189
2190 pub fn writeVersionInfo(self: *Compiler, node: *Node.VersionInfo, writer: anytype) !void {
2191 var data_buffer = std.ArrayList(u8).init(self.allocator);
2192 defer data_buffer.deinit();
2193 // The node's length field (which is inclusive of the length of all of its children) is a u16
2194 // so limit the node's data size so that we know we can always specify the real size.
2195 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u16));
2196 const data_writer = limited_writer.writer();
2197
2198 try data_writer.writeIntLittle(u16, 0); // placeholder size
2199 try data_writer.writeIntLittle(u16, res.FixedFileInfo.byte_len);
2200 try data_writer.writeIntLittle(u16, res.VersionNode.type_binary);
2201 const key_bytes = std.mem.sliceAsBytes(res.FixedFileInfo.key[0 .. res.FixedFileInfo.key.len + 1]);
2202 try data_writer.writeAll(key_bytes);
2203 // The number of bytes written up to this point is always the same, since the name
2204 // of the node is a constant (FixedFileInfo.key). The total number of bytes
2205 // written so far is 38, so we need 2 padding bytes to get back to DWORD alignment
2206 try data_writer.writeIntLittle(u16, 0);
2207
2208 var fixed_file_info = res.FixedFileInfo{};
2209 for (node.fixed_info) |fixed_info| {
2210 switch (fixed_info.id) {
2211 .version_statement => {
2212 const version_statement = @fieldParentPtr(Node.VersionStatement, "base", fixed_info);
2213 const version_type = rc.VersionInfo.map.get(version_statement.type.slice(self.source)).?;
2214
2215 // Ensure that all parts are cleared for each version, to properly account for
2216 // potential duplicate PRODUCTVERSION/FILEVERSION statements
2217 switch (version_type) {
2218 .file_version => @memset(&fixed_file_info.file_version.parts, 0),
2219 .product_version => @memset(&fixed_file_info.product_version.parts, 0),
2220 else => unreachable,
2221 }
2222
2223 for (version_statement.parts, 0..) |part, i| {
2224 const part_value = evaluateNumberExpression(part, self.source, self.input_code_pages);
2225 if (part_value.is_long) {
2226 try self.addErrorDetails(.{
2227 .err = .rc_would_error_u16_with_l_suffix,
2228 .type = .warning,
2229 .token = part.getFirstToken(),
2230 .token_span_end = part.getLastToken(),
2231 .extra = .{ .statement_with_u16_param = switch (version_type) {
2232 .file_version => .fileversion,
2233 .product_version => .productversion,
2234 else => unreachable,
2235 } },
2236 });
2237 try self.addErrorDetails(.{
2238 .err = .rc_would_error_u16_with_l_suffix,
2239 .print_source_line = false,
2240 .type = .note,
2241 .token = part.getFirstToken(),
2242 .token_span_end = part.getLastToken(),
2243 .extra = .{ .statement_with_u16_param = switch (version_type) {
2244 .file_version => .fileversion,
2245 .product_version => .productversion,
2246 else => unreachable,
2247 } },
2248 });
2249 }
2250 switch (version_type) {
2251 .file_version => {
2252 fixed_file_info.file_version.parts[i] = part_value.asWord();
2253 },
2254 .product_version => {
2255 fixed_file_info.product_version.parts[i] = part_value.asWord();
2256 },
2257 else => unreachable,
2258 }
2259 }
2260 },
2261 .simple_statement => {
2262 const statement = @fieldParentPtr(Node.SimpleStatement, "base", fixed_info);
2263 const statement_type = rc.VersionInfo.map.get(statement.identifier.slice(self.source)).?;
2264 const value = evaluateNumberExpression(statement.value, self.source, self.input_code_pages);
2265 switch (statement_type) {
2266 .file_flags_mask => fixed_file_info.file_flags_mask = value.value,
2267 .file_flags => fixed_file_info.file_flags = value.value,
2268 .file_os => fixed_file_info.file_os = value.value,
2269 .file_type => fixed_file_info.file_type = value.value,
2270 .file_subtype => fixed_file_info.file_subtype = value.value,
2271 else => unreachable,
2272 }
2273 },
2274 else => unreachable,
2275 }
2276 }
2277 try fixed_file_info.write(data_writer);
2278
2279 for (node.block_statements) |statement| {
2280 self.writeVersionNode(statement, data_writer, &data_buffer) catch |err| switch (err) {
2281 error.NoSpaceLeft => {
2282 try self.addErrorDetails(.{
2283 .err = .version_node_size_exceeds_max,
2284 .token = node.id,
2285 });
2286 return self.addErrorDetailsAndFail(.{
2287 .err = .version_node_size_exceeds_max,
2288 .type = .note,
2289 .token = statement.getFirstToken(),
2290 .token_span_end = statement.getLastToken(),
2291 });
2292 },
2293 else => |e| return e,
2294 };
2295 }
2296
2297 // We know that data_buffer.items.len is within the limits of a u16, since we
2298 // limited the writer to maxInt(u16)
2299 const data_size: u16 = @intCast(data_buffer.items.len);
2300 // And now that we know the full size of this node (including its children), set its size
2301 std.mem.writeIntLittle(u16, data_buffer.items[0..2], data_size);
2302
2303 var header = try self.resourceHeader(node.id, node.versioninfo, .{
2304 .data_size = data_size,
2305 });
2306 defer header.deinit(self.allocator);
2307
2308 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2309
2310 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
2311
2312 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2313 try writeResourceData(writer, data_fbs.reader(), data_size);
2314 }
2315
2316 /// Expects writer to be a LimitedWriter limited to u16, meaning all writes to
2317 /// the writer within this function could return error.NoSpaceLeft, and that buf.items.len
2318 /// will never be able to exceed maxInt(u16).
2319 pub fn writeVersionNode(self: *Compiler, node: *Node, writer: anytype, buf: *std.ArrayList(u8)) !void {
2320 // We can assume that buf.items.len will never be able to exceed the limits of a u16
2321 try writeDataPadding(writer, @as(u16, @intCast(buf.items.len)));
2322
2323 const node_and_children_size_offset = buf.items.len;
2324 try writer.writeIntLittle(u16, 0); // placeholder for size
2325 const data_size_offset = buf.items.len;
2326 try writer.writeIntLittle(u16, 0); // placeholder for data size
2327 const data_type_offset = buf.items.len;
2328 // Data type is string unless the node contains values that are numbers.
2329 try writer.writeIntLittle(u16, res.VersionNode.type_string);
2330
2331 switch (node.id) {
2332 inline .block, .block_value => |node_type| {
2333 const block_or_value = @fieldParentPtr(node_type.Type(), "base", node);
2334 const parsed_key = try self.parseQuotedStringAsWideString(block_or_value.key);
2335 defer self.allocator.free(parsed_key);
2336
2337 const parsed_key_to_first_null = std.mem.sliceTo(parsed_key, 0);
2338 try writer.writeAll(std.mem.sliceAsBytes(parsed_key_to_first_null[0 .. parsed_key_to_first_null.len + 1]));
2339
2340 var has_number_value: bool = false;
2341 for (block_or_value.values) |value_value_node_uncasted| {
2342 const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
2343 if (value_value_node.expression.isNumberExpression()) {
2344 has_number_value = true;
2345 break;
2346 }
2347 }
2348 // The units used here are dependent on the type. If there are any numbers, then
2349 // this is a byte count. If there are only strings, then this is a count of
2350 // UTF-16 code units.
2351 //
2352 // The Win32 RC compiler miscompiles this count in the case of values that
2353 // have a mix of numbers and strings. This is detected and a warning is emitted
2354 // during parsing, so we can just do the correct thing here.
2355 var values_size: usize = 0;
2356
2357 try writeDataPadding(writer, @intCast(buf.items.len));
2358
2359 for (block_or_value.values, 0..) |value_value_node_uncasted, i| {
2360 const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
2361 const value_node = value_value_node.expression;
2362 if (value_node.isNumberExpression()) {
2363 const number = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
2364 // This is used to write u16 or u32 depending on the number's suffix
2365 const data_wrapper = Data{ .number = number };
2366 try data_wrapper.write(writer);
2367 // Numbers use byte count
2368 values_size += if (number.is_long) 4 else 2;
2369 } else {
2370 std.debug.assert(value_node.isStringLiteral());
2371 const literal_node = value_node.cast(.literal).?;
2372 const parsed_value = try self.parseQuotedStringAsWideString(literal_node.token);
2373 defer self.allocator.free(parsed_value);
2374
2375 const parsed_to_first_null = std.mem.sliceTo(parsed_value, 0);
2376 try writer.writeAll(std.mem.sliceAsBytes(parsed_to_first_null));
2377 // Strings use UTF-16 code-unit count including the null-terminator, but
2378 // only if there are no number values in the list.
2379 var value_size = parsed_to_first_null.len;
2380 if (has_number_value) value_size *= 2; // 2 bytes per UTF-16 code unit
2381 values_size += value_size;
2382 // The null-terminator is only included if there's a trailing comma
2383 // or this is the last value. If the value evaluates to empty, then
2384 // it never gets a null terminator. If there was an explicit null-terminator
2385 // in the string, we still need to potentially add one since we already
2386 // sliced to the terminator.
2387 const is_last = i == block_or_value.values.len - 1;
2388 const is_empty = parsed_to_first_null.len == 0;
2389 const is_only = block_or_value.values.len == 1;
2390 if ((!is_empty or !is_only) and (is_last or value_value_node.trailing_comma)) {
2391 try writer.writeIntLittle(u16, 0);
2392 values_size += if (has_number_value) 2 else 1;
2393 }
2394 }
2395 }
2396 var data_size_slice = buf.items[data_size_offset..];
2397 std.mem.writeIntLittle(u16, data_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(values_size)));
2398
2399 if (has_number_value) {
2400 const data_type_slice = buf.items[data_type_offset..];
2401 std.mem.writeIntLittle(u16, data_type_slice[0..@sizeOf(u16)], res.VersionNode.type_binary);
2402 }
2403
2404 if (node_type == .block) {
2405 const block = block_or_value;
2406 for (block.children) |child| {
2407 try self.writeVersionNode(child, writer, buf);
2408 }
2409 }
2410 },
2411 else => unreachable,
2412 }
2413
2414 const node_and_children_size = buf.items.len - node_and_children_size_offset;
2415 const node_and_children_size_slice = buf.items[node_and_children_size_offset..];
2416 std.mem.writeIntLittle(u16, node_and_children_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(node_and_children_size)));
2417 }
2418
2419 pub fn writeStringTable(self: *Compiler, node: *Node.StringTable) !void {
2420 const language = getLanguageFromOptionalStatements(node.optional_statements, self.source, self.input_code_pages) orelse self.state.language;
2421
2422 for (node.strings) |string_node| {
2423 const string = @fieldParentPtr(Node.StringTableString, "base", string_node);
2424 const string_id_data = try self.evaluateDataExpression(string.id);
2425 const string_id = string_id_data.number.asWord();
2426
2427 self.state.string_tables.set(
2428 self.arena,
2429 language,
2430 string_id,
2431 string.string,
2432 &node.base,
2433 self.source,
2434 self.input_code_pages,
2435 self.state.version,
2436 self.state.characteristics,
2437 ) catch |err| switch (err) {
2438 error.StringAlreadyDefined => {
2439 // It might be nice to have these errors point to the ids rather than the
2440 // string tokens, but that would mean storing the id token of each string
2441 // which doesn't seem worth it just for slightly better error messages.
2442 try self.addErrorDetails(ErrorDetails{
2443 .err = .string_already_defined,
2444 .token = string.string,
2445 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
2446 });
2447 const existing_def_table = self.state.string_tables.tables.getPtr(language).?;
2448 const existing_definition = existing_def_table.get(string_id).?;
2449 return self.addErrorDetailsAndFail(ErrorDetails{
2450 .err = .string_already_defined,
2451 .type = .note,
2452 .token = existing_definition,
2453 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
2454 });
2455 },
2456 error.OutOfMemory => |e| return e,
2457 };
2458 }
2459 }
2460
2461 /// Expects this to be a top-level LANGUAGE statement
2462 pub fn writeLanguageStatement(self: *Compiler, node: *Node.LanguageStatement) void {
2463 const primary = Compiler.evaluateNumberExpression(node.primary_language_id, self.source, self.input_code_pages);
2464 const sublanguage = Compiler.evaluateNumberExpression(node.sublanguage_id, self.source, self.input_code_pages);
2465 self.state.language.primary_language_id = @truncate(primary.value);
2466 self.state.language.sublanguage_id = @truncate(sublanguage.value);
2467 }
2468
2469 /// Expects this to be a top-level VERSION or CHARACTERISTICS statement
2470 pub fn writeTopLevelSimpleStatement(self: *Compiler, node: *Node.SimpleStatement) void {
2471 const value = Compiler.evaluateNumberExpression(node.value, self.source, self.input_code_pages);
2472 const statement_type = rc.TopLevelKeywords.map.get(node.identifier.slice(self.source)).?;
2473 switch (statement_type) {
2474 .characteristics => self.state.characteristics = value.value,
2475 .version => self.state.version = value.value,
2476 else => unreachable,
2477 }
2478 }
2479
2480 pub const ResourceHeaderOptions = struct {
2481 language: ?res.Language = null,
2482 data_size: DWORD = 0,
2483 };
2484
2485 pub fn resourceHeader(self: *Compiler, id_token: Token, type_token: Token, options: ResourceHeaderOptions) !ResourceHeader {
2486 const id_bytes = self.sourceBytesForToken(id_token);
2487 const type_bytes = self.sourceBytesForToken(type_token);
2488 return ResourceHeader.init(
2489 self.allocator,
2490 id_bytes,
2491 type_bytes,
2492 options.data_size,
2493 options.language orelse self.state.language,
2494 self.state.version,
2495 self.state.characteristics,
2496 ) catch |err| switch (err) {
2497 error.OutOfMemory => |e| return e,
2498 error.TypeNonAsciiOrdinal => {
2499 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes).?;
2500 try self.addErrorDetails(.{
2501 .err = .invalid_digit_character_in_ordinal,
2502 .type = .err,
2503 .token = type_token,
2504 });
2505 return self.addErrorDetailsAndFail(.{
2506 .err = .win32_non_ascii_ordinal,
2507 .type = .note,
2508 .token = type_token,
2509 .print_source_line = false,
2510 .extra = .{ .number = win32_rc_ordinal.ordinal },
2511 });
2512 },
2513 error.IdNonAsciiOrdinal => {
2514 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes).?;
2515 try self.addErrorDetails(.{
2516 .err = .invalid_digit_character_in_ordinal,
2517 .type = .err,
2518 .token = id_token,
2519 });
2520 return self.addErrorDetailsAndFail(.{
2521 .err = .win32_non_ascii_ordinal,
2522 .type = .note,
2523 .token = id_token,
2524 .print_source_line = false,
2525 .extra = .{ .number = win32_rc_ordinal.ordinal },
2526 });
2527 },
2528 };
2529 }
2530
2531 pub const ResourceHeader = struct {
2532 name_value: NameOrOrdinal,
2533 type_value: NameOrOrdinal,
2534 language: res.Language,
2535 memory_flags: MemoryFlags,
2536 data_size: DWORD,
2537 version: DWORD,
2538 characteristics: DWORD,
2539 data_version: DWORD = 0,
2540
2541 pub const InitError = error{ OutOfMemory, IdNonAsciiOrdinal, TypeNonAsciiOrdinal };
2542
2543 pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {
2544 const type_value = type: {
2545 const resource_type = Resource.fromString(type_bytes);
2546 if (res.RT.fromResource(resource_type)) |rt_constant| {
2547 break :type NameOrOrdinal{ .ordinal = @intFromEnum(rt_constant) };
2548 } else {
2549 break :type try NameOrOrdinal.fromString(allocator, type_bytes);
2550 }
2551 };
2552 errdefer type_value.deinit(allocator);
2553 if (type_value == .name) {
2554 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes)) |_| {
2555 return error.TypeNonAsciiOrdinal;
2556 }
2557 }
2558
2559 const name_value = try NameOrOrdinal.fromString(allocator, id_bytes);
2560 errdefer name_value.deinit(allocator);
2561 if (name_value == .name) {
2562 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes)) |_| {
2563 return error.IdNonAsciiOrdinal;
2564 }
2565 }
2566
2567 const predefined_resource_type = type_value.predefinedResourceType();
2568
2569 return ResourceHeader{
2570 .name_value = name_value,
2571 .type_value = type_value,
2572 .data_size = data_size,
2573 .memory_flags = MemoryFlags.defaults(predefined_resource_type),
2574 .language = language,
2575 .version = version,
2576 .characteristics = characteristics,
2577 };
2578 }
2579
2580 pub fn deinit(self: ResourceHeader, allocator: Allocator) void {
2581 self.name_value.deinit(allocator);
2582 self.type_value.deinit(allocator);
2583 }
2584
2585 pub const SizeInfo = struct {
2586 bytes: u32,
2587 padding_after_name: u2,
2588 };
2589
2590 fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {
2591 var header_size: u32 = 8;
2592 header_size = try std.math.add(
2593 u32,
2594 header_size,
2595 std.math.cast(u32, self.name_value.byteLen()) orelse return error.Overflow,
2596 );
2597 header_size = try std.math.add(
2598 u32,
2599 header_size,
2600 std.math.cast(u32, self.type_value.byteLen()) orelse return error.Overflow,
2601 );
2602 const padding_after_name = numPaddingBytesNeeded(header_size);
2603 header_size = try std.math.add(u32, header_size, padding_after_name);
2604 header_size = try std.math.add(u32, header_size, 16);
2605 return .{ .bytes = header_size, .padding_after_name = padding_after_name };
2606 }
2607
2608 pub fn writeAssertNoOverflow(self: ResourceHeader, writer: anytype) !void {
2609 return self.writeSizeInfo(writer, self.calcSize() catch unreachable);
2610 }
2611
2612 pub fn write(self: ResourceHeader, writer: anytype, err_ctx: errors.DiagnosticsContext) !void {
2613 const size_info = self.calcSize() catch {
2614 try err_ctx.diagnostics.append(.{
2615 .err = .resource_data_size_exceeds_max,
2616 .token = err_ctx.token,
2617 });
2618 return error.CompileError;
2619 };
2620 return self.writeSizeInfo(writer, size_info);
2621 }
2622
2623 fn writeSizeInfo(self: ResourceHeader, writer: anytype, size_info: SizeInfo) !void {
2624 try writer.writeIntLittle(DWORD, self.data_size); // DataSize
2625 try writer.writeIntLittle(DWORD, size_info.bytes); // HeaderSize
2626 try self.type_value.write(writer); // TYPE
2627 try self.name_value.write(writer); // NAME
2628 try writer.writeByteNTimes(0, size_info.padding_after_name);
2629
2630 try writer.writeIntLittle(DWORD, self.data_version); // DataVersion
2631 try writer.writeIntLittle(WORD, self.memory_flags.value); // MemoryFlags
2632 try writer.writeIntLittle(WORD, self.language.asInt()); // LanguageId
2633 try writer.writeIntLittle(DWORD, self.version); // Version
2634 try writer.writeIntLittle(DWORD, self.characteristics); // Characteristics
2635 }
2636
2637 pub fn predefinedResourceType(self: ResourceHeader) ?res.RT {
2638 return self.type_value.predefinedResourceType();
2639 }
2640
2641 pub fn applyMemoryFlags(self: *ResourceHeader, tokens: []Token, source: []const u8) void {
2642 applyToMemoryFlags(&self.memory_flags, tokens, source);
2643 }
2644
2645 pub fn applyOptionalStatements(self: *ResourceHeader, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
2646 applyToOptionalStatements(&self.language, &self.version, &self.characteristics, statements, source, code_page_lookup);
2647 }
2648 };
2649
2650 fn applyToMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
2651 for (tokens) |token| {
2652 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2653 flags.set(attribute);
2654 }
2655 }
2656
2657 /// RT_GROUP_ICON and RT_GROUP_CURSOR have their own special rules for memory flags
2658 fn applyToGroupMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
2659 // There's probably a cleaner implementation of this, but this will result in the same
2660 // flags as the Win32 RC compiler for all 986,410 K-permutations of memory flags
2661 // for an ICON resource.
2662 //
2663 // This was arrived at by iterating over the permutations and creating a
2664 // list where each line looks something like this:
2665 // MOVEABLE PRELOAD -> 0x1050 (MOVEABLE|PRELOAD|DISCARDABLE)
2666 //
2667 // and then noticing a few things:
2668
2669 // 1. Any permutation that does not have PRELOAD in it just uses the
2670 // default flags.
2671 const initial_flags = flags.*;
2672 var flags_set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
2673 for (tokens) |token| {
2674 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2675 flags_set.insert(attribute);
2676 }
2677 if (!flags_set.contains(.preload)) return;
2678
2679 // 2. Any permutation of flags where applying only the PRELOAD and LOADONCALL flags
2680 // results in no actual change by the end will just use the default flags.
2681 // For example, `PRELOAD LOADONCALL` will result in default flags, but
2682 // `LOADONCALL PRELOAD` will have PRELOAD set after they are both applied in order.
2683 for (tokens) |token| {
2684 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2685 switch (attribute) {
2686 .preload, .loadoncall => flags.set(attribute),
2687 else => {},
2688 }
2689 }
2690 if (flags.value == initial_flags.value) return;
2691
2692 // 3. If none of DISCARDABLE, SHARED, or PURE is specified, then PRELOAD
2693 // implies `flags &= ~SHARED` and LOADONCALL implies `flags |= SHARED`
2694 const shared_set = comptime blk: {
2695 var set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
2696 set.insert(.discardable);
2697 set.insert(.shared);
2698 set.insert(.pure);
2699 break :blk set;
2700 };
2701 const discardable_shared_or_pure_specified = flags_set.intersectWith(shared_set).count() != 0;
2702 for (tokens) |token| {
2703 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2704 flags.setGroup(attribute, !discardable_shared_or_pure_specified);
2705 }
2706 }
2707
2708 /// Only handles the 'base' optional statements that are shared between resource types.
2709 fn applyToOptionalStatements(language: *res.Language, version: *u32, characteristics: *u32, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
2710 for (statements) |node| switch (node.id) {
2711 .language_statement => {
2712 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
2713 language.* = languageFromLanguageStatement(language_statement, source, code_page_lookup);
2714 },
2715 .simple_statement => {
2716 const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", node);
2717 const statement_type = rc.OptionalStatements.map.get(simple_statement.identifier.slice(source)) orelse continue;
2718 const result = Compiler.evaluateNumberExpression(simple_statement.value, source, code_page_lookup);
2719 switch (statement_type) {
2720 .version => version.* = result.value,
2721 .characteristics => characteristics.* = result.value,
2722 else => unreachable, // only VERSION and CHARACTERISTICS should be in an optional statements list
2723 }
2724 },
2725 else => {},
2726 };
2727 }
2728
2729 pub fn languageFromLanguageStatement(language_statement: *const Node.LanguageStatement, source: []const u8, code_page_lookup: *const CodePageLookup) res.Language {
2730 const primary = Compiler.evaluateNumberExpression(language_statement.primary_language_id, source, code_page_lookup);
2731 const sublanguage = Compiler.evaluateNumberExpression(language_statement.sublanguage_id, source, code_page_lookup);
2732 return .{
2733 .primary_language_id = @truncate(primary.value),
2734 .sublanguage_id = @truncate(sublanguage.value),
2735 };
2736 }
2737
2738 pub fn getLanguageFromOptionalStatements(statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) ?res.Language {
2739 for (statements) |node| switch (node.id) {
2740 .language_statement => {
2741 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
2742 return languageFromLanguageStatement(language_statement, source, code_page_lookup);
2743 },
2744 else => continue,
2745 };
2746 return null;
2747 }
2748
2749 pub fn writeEmptyResource(writer: anytype) !void {
2750 const header = ResourceHeader{
2751 .name_value = .{ .ordinal = 0 },
2752 .type_value = .{ .ordinal = 0 },
2753 .language = .{
2754 .primary_language_id = 0,
2755 .sublanguage_id = 0,
2756 },
2757 .memory_flags = .{ .value = 0 },
2758 .data_size = 0,
2759 .version = 0,
2760 .characteristics = 0,
2761 };
2762 try header.writeAssertNoOverflow(writer);
2763 }
2764
2765 pub fn sourceBytesForToken(self: *Compiler, token: Token) SourceBytes {
2766 return .{
2767 .slice = token.slice(self.source),
2768 .code_page = self.input_code_pages.getForToken(token),
2769 };
2770 }
2771
2772 /// Helper that calls parseQuotedStringAsWideString with the relevant context
2773 /// Resulting slice is allocated by `self.allocator`.
2774 pub fn parseQuotedStringAsWideString(self: *Compiler, token: Token) ![:0]u16 {
2775 return literals.parseQuotedStringAsWideString(
2776 self.allocator,
2777 self.sourceBytesForToken(token),
2778 .{
2779 .start_column = token.calculateColumn(self.source, 8, null),
2780 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
2781 },
2782 );
2783 }
2784
2785 /// Helper that calls parseQuotedStringAsAsciiString with the relevant context
2786 /// Resulting slice is allocated by `self.allocator`.
2787 pub fn parseQuotedStringAsAsciiString(self: *Compiler, token: Token) ![]u8 {
2788 return literals.parseQuotedStringAsAsciiString(
2789 self.allocator,
2790 self.sourceBytesForToken(token),
2791 .{
2792 .start_column = token.calculateColumn(self.source, 8, null),
2793 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
2794 },
2795 );
2796 }
2797
2798 fn addErrorDetails(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
2799 try self.diagnostics.append(details);
2800 }
2801
2802 fn addErrorDetailsAndFail(self: *Compiler, details: ErrorDetails) error{ CompileError, OutOfMemory } {
2803 try self.addErrorDetails(details);
2804 return error.CompileError;
2805 }
2806};
2807
2808pub const OpenSearchPathError = std.fs.Dir.OpenError;
2809
2810fn openSearchPathDir(dir: std.fs.Dir, path: []const u8) OpenSearchPathError!std.fs.Dir {
2811 // Validate the search path to avoid possible unreachable on invalid paths,
2812 // see https://github.com/ziglang/zig/issues/15607 for why this is currently necessary.
2813 try validateSearchPath(path);
2814 return dir.openDir(path, .{});
2815}
2816
2817/// Very crude attempt at validating a path. This is imperfect
2818/// and AFAIK it is effectively impossible to implement perfect path
2819/// validation, since it ultimately depends on the underlying filesystem.
2820/// Note that this function won't be necessary if/when
2821/// https://github.com/ziglang/zig/issues/15607
2822/// is accepted/implemented.
2823fn validateSearchPath(path: []const u8) error{BadPathName}!void {
2824 switch (builtin.os.tag) {
2825 .windows => {
2826 // This will return error.BadPathName on non-Win32 namespaced paths
2827 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
2828 // Those path types are something of an unavoidable way to
2829 // still hit unreachable during the openDir call.
2830 var component_iterator = try std.fs.path.componentIterator(path);
2831 while (component_iterator.next()) |component| {
2832 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2833 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
2834 }
2835 },
2836 else => {
2837 if (std.mem.indexOfScalar(u8, path, 0) != null) return error.BadPathName;
2838 },
2839 }
2840}
2841
2842pub const SearchDir = struct {
2843 dir: std.fs.Dir,
2844 path: ?[]const u8,
2845
2846 pub fn deinit(self: *SearchDir, allocator: Allocator) void {
2847 self.dir.close();
2848 if (self.path) |path| {
2849 allocator.free(path);
2850 }
2851 }
2852};
2853
2854/// Slurps the first `size` bytes read into `slurped_header`
2855pub fn HeaderSlurpingReader(comptime size: usize, comptime ReaderType: anytype) type {
2856 return struct {
2857 child_reader: ReaderType,
2858 bytes_read: u64 = 0,
2859 slurped_header: [size]u8 = [_]u8{0x00} ** size,
2860
2861 pub const Error = ReaderType.Error;
2862 pub const Reader = std.io.Reader(*@This(), Error, read);
2863
2864 pub fn read(self: *@This(), buf: []u8) Error!usize {
2865 const amt = try self.child_reader.read(buf);
2866 if (self.bytes_read < size) {
2867 const bytes_to_add = @min(amt, size - self.bytes_read);
2868 const end_index = self.bytes_read + bytes_to_add;
2869 std.mem.copy(u8, self.slurped_header[self.bytes_read..end_index], buf[0..bytes_to_add]);
2870 }
2871 self.bytes_read += amt;
2872 return amt;
2873 }
2874
2875 pub fn reader(self: *@This()) Reader {
2876 return .{ .context = self };
2877 }
2878 };
2879}
2880
2881pub fn headerSlurpingReader(comptime size: usize, reader: anytype) HeaderSlurpingReader(size, @TypeOf(reader)) {
2882 return .{ .child_reader = reader };
2883}
2884
2885/// Sort of like std.io.LimitedReader, but a Writer.
2886/// Returns an error if writing the requested number of bytes
2887/// would ever exceed bytes_left, i.e. it does not always
2888/// write up to the limit and instead will error if the
2889/// limit would be breached if the entire slice was written.
2890pub fn LimitedWriter(comptime WriterType: type) type {
2891 return struct {
2892 inner_writer: WriterType,
2893 bytes_left: u64,
2894
2895 pub const Error = error{NoSpaceLeft} || WriterType.Error;
2896 pub const Writer = std.io.Writer(*Self, Error, write);
2897
2898 const Self = @This();
2899
2900 pub fn write(self: *Self, bytes: []const u8) Error!usize {
2901 if (bytes.len > self.bytes_left) return error.NoSpaceLeft;
2902 const amt = try self.inner_writer.write(bytes);
2903 self.bytes_left -= amt;
2904 return amt;
2905 }
2906
2907 pub fn writer(self: *Self) Writer {
2908 return .{ .context = self };
2909 }
2910 };
2911}
2912
2913/// Returns an initialised `LimitedWriter`
2914/// `bytes_left` is a `u64` to be able to take 64 bit file offsets
2915pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) {
2916 return .{ .inner_writer = inner_writer, .bytes_left = bytes_left };
2917}
2918
2919test "limitedWriter basic usage" {
2920 var buf: [4]u8 = undefined;
2921 var fbs = std.io.fixedBufferStream(&buf);
2922 var limited_stream = limitedWriter(fbs.writer(), 4);
2923 var writer = limited_stream.writer();
2924
2925 try std.testing.expectEqual(@as(usize, 3), try writer.write("123"));
2926 try std.testing.expectEqualSlices(u8, "123", buf[0..3]);
2927 try std.testing.expectError(error.NoSpaceLeft, writer.write("45"));
2928 try std.testing.expectEqual(@as(usize, 1), try writer.write("4"));
2929 try std.testing.expectEqualSlices(u8, "1234", buf[0..4]);
2930 try std.testing.expectError(error.NoSpaceLeft, writer.write("5"));
2931}
2932
2933pub const FontDir = struct {
2934 fonts: std.ArrayListUnmanaged(Font) = .{},
2935 /// To keep track of which ids are set and where they were set from
2936 ids: std.AutoHashMapUnmanaged(u16, Token) = .{},
2937
2938 pub const Font = struct {
2939 id: u16,
2940 header_bytes: [148]u8,
2941 };
2942
2943 pub fn deinit(self: *FontDir, allocator: Allocator) void {
2944 self.fonts.deinit(allocator);
2945 }
2946
2947 pub fn add(self: *FontDir, allocator: Allocator, font: Font, id_token: Token) !void {
2948 try self.ids.putNoClobber(allocator, font.id, id_token);
2949 try self.fonts.append(allocator, font);
2950 }
2951
2952 pub fn writeResData(self: *FontDir, compiler: *Compiler, writer: anytype) !void {
2953 if (self.fonts.items.len == 0) return;
2954
2955 // We know the number of fonts is limited to maxInt(u16) because fonts
2956 // must have a valid and unique u16 ordinal ID (trying to specify a FONT
2957 // with e.g. id 65537 will wrap around to 1 and be ignored if there's already
2958 // a font with that ID in the file).
2959 const num_fonts: u16 = @intCast(self.fonts.items.len);
2960
2961 // u16 count + [(u16 id + 150 bytes) for each font]
2962 // Note: This works out to a maximum data_size of 9,961,322.
2963 const data_size: u32 = 2 + (2 + 150) * num_fonts;
2964
2965 var header = Compiler.ResourceHeader{
2966 .name_value = try NameOrOrdinal.nameFromString(compiler.allocator, .{ .slice = "FONTDIR", .code_page = .windows1252 }),
2967 .type_value = NameOrOrdinal{ .ordinal = @intFromEnum(res.RT.FONTDIR) },
2968 .memory_flags = res.MemoryFlags.defaults(res.RT.FONTDIR),
2969 .language = compiler.state.language,
2970 .version = compiler.state.version,
2971 .characteristics = compiler.state.characteristics,
2972 .data_size = data_size,
2973 };
2974 defer header.deinit(compiler.allocator);
2975
2976 try header.writeAssertNoOverflow(writer);
2977 try writer.writeIntLittle(u16, num_fonts);
2978 for (self.fonts.items) |font| {
2979 // The format of the FONTDIR is a strange beast.
2980 // Technically, each FONT is seemingly meant to be written as a
2981 // FONTDIRENTRY with two trailing NUL-terminated strings corresponding to
2982 // the 'device name' and 'face name' of the .FNT file, but:
2983 //
2984 // 1. When dealing with .FNT files, the Win32 implementation
2985 // gets the device name and face name from the wrong locations,
2986 // so it's basically never going to write the real device/face name
2987 // strings.
2988 // 2. When dealing with files 76-140 bytes long, the Win32 implementation
2989 // can just crash (if there are no NUL bytes in the file).
2990 // 3. The 32-bit Win32 rc.exe uses a 148 byte size for the portion of
2991 // the FONTDIRENTRY before the NUL-terminated strings, which
2992 // does not match the documented FONTDIRENTRY size that (presumably)
2993 // this format is meant to be using, so anything iterating the
2994 // FONTDIR according to the available documentation will get bogus results.
2995 // 4. The FONT resource can be used for non-.FNT types like TTF and OTF,
2996 // in which case emulating the Win32 behavior of unconditionally
2997 // interpreting the bytes as a .FNT and trying to grab device/face names
2998 // from random bytes in the TTF/OTF file can lead to weird behavior
2999 // and errors in the Win32 implementation (for example, the device/face
3000 // name fields are offsets into the file where the NUL-terminated
3001 // string is located, but the Win32 implementation actually treats
3002 // them as signed so if they are negative then the Win32 implementation
3003 // will error; this happening for TTF fonts would just be a bug
3004 // since the TTF could otherwise be valid)
3005 // 5. The FONTDIR resource doesn't actually seem to be used at all by
3006 // anything that I've found, and instead in Windows 3.0 and newer
3007 // it seems like the FONT resources are always just iterated/accessed
3008 // directly without ever looking at the FONTDIR.
3009 //
3010 // All of these combined means that we:
3011 // - Do not need or want to emulate Win32 behavior here
3012 // - For maximum simplicity and compatibility, we just write the first
3013 // 148 bytes of the file without any interpretation (padded with
3014 // zeroes to get up to 148 bytes if necessary), and then
3015 // unconditionally write two NUL bytes, meaning that we always
3016 // write 'device name' and 'face name' as if they were 0-length
3017 // strings.
3018 //
3019 // This gives us byte-for-byte .RES compatibility in the common case while
3020 // allowing us to avoid any erroneous errors caused by trying to read
3021 // the face/device name from a bogus location. Note that the Win32
3022 // implementation never actually writes the real device/face name here
3023 // anyway (except in the bizarre case that a .FNT file has the proper
3024 // device/face name offsets within a reserved section of the .FNT file)
3025 // so there's no feasible way that anything can actually think that the
3026 // device name/face name in the FONTDIR is reliable.
3027
3028 // First, the ID is written, though
3029 try writer.writeIntLittle(u16, font.id);
3030 try writer.writeAll(&font.header_bytes);
3031 try writer.writeByteNTimes(0, 2);
3032 }
3033 try Compiler.writeDataPadding(writer, data_size);
3034 }
3035};
3036
3037pub const StringTablesByLanguage = struct {
3038 /// String tables for each language are written to the .res file in order depending on
3039 /// when the first STRINGTABLE for the language was defined, and all blocks for a given
3040 /// language are written contiguously.
3041 /// Using an ArrayHashMap here gives us this property for free.
3042 tables: std.AutoArrayHashMapUnmanaged(res.Language, StringTable) = .{},
3043
3044 pub fn deinit(self: *StringTablesByLanguage, allocator: Allocator) void {
3045 self.tables.deinit(allocator);
3046 }
3047
3048 pub fn set(
3049 self: *StringTablesByLanguage,
3050 allocator: Allocator,
3051 language: res.Language,
3052 id: u16,
3053 string_token: Token,
3054 node: *Node,
3055 source: []const u8,
3056 code_page_lookup: *const CodePageLookup,
3057 version: u32,
3058 characteristics: u32,
3059 ) StringTable.SetError!void {
3060 var get_or_put_result = try self.tables.getOrPut(allocator, language);
3061 if (!get_or_put_result.found_existing) {
3062 get_or_put_result.value_ptr.* = StringTable{};
3063 }
3064 return get_or_put_result.value_ptr.set(allocator, id, string_token, node, source, code_page_lookup, version, characteristics);
3065 }
3066};
3067
3068pub const StringTable = struct {
3069 /// Blocks are written to the .res file in order depending on when the first string
3070 /// was added to the block (i.e. `STRINGTABLE { 16 "b" 0 "a" }` would then get written
3071 /// with block ID 2 (the one with "b") first and block ID 1 (the one with "a") second).
3072 /// Using an ArrayHashMap here gives us this property for free.
3073 blocks: std.AutoArrayHashMapUnmanaged(u16, Block) = .{},
3074
3075 pub const Block = struct {
3076 strings: std.ArrayListUnmanaged(Token) = .{},
3077 set_indexes: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 },
3078 memory_flags: MemoryFlags = MemoryFlags.defaults(res.RT.STRING),
3079 characteristics: u32,
3080 version: u32,
3081
3082 /// Returns the index to insert the string into the `strings` list.
3083 /// Returns null if the string should be appended.
3084 fn getInsertionIndex(self: *Block, index: u8) ?u8 {
3085 std.debug.assert(!self.set_indexes.isSet(index));
3086
3087 const first_set = self.set_indexes.findFirstSet() orelse return null;
3088 if (first_set > index) return 0;
3089
3090 const last_set = 15 - @clz(self.set_indexes.mask);
3091 if (index > last_set) return null;
3092
3093 var bit = first_set + 1;
3094 var insertion_index: u8 = 1;
3095 while (bit != index) : (bit += 1) {
3096 if (self.set_indexes.isSet(bit)) insertion_index += 1;
3097 }
3098 return insertion_index;
3099 }
3100
3101 fn getTokenIndex(self: *Block, string_index: u8) ?u8 {
3102 const count = self.strings.items.len;
3103 if (count == 0) return null;
3104 if (count == 1) return 0;
3105
3106 const first_set = self.set_indexes.findFirstSet() orelse unreachable;
3107 if (first_set == string_index) return 0;
3108 const last_set = 15 - @clz(self.set_indexes.mask);
3109 if (last_set == string_index) return @intCast(count - 1);
3110
3111 if (first_set == last_set) return null;
3112
3113 var bit = first_set + 1;
3114 var token_index: u8 = 1;
3115 while (bit < last_set) : (bit += 1) {
3116 if (!self.set_indexes.isSet(bit)) continue;
3117 if (bit == string_index) return token_index;
3118 token_index += 1;
3119 }
3120 return null;
3121 }
3122
3123 fn dump(self: *Block) void {
3124 var bit_it = self.set_indexes.iterator(.{});
3125 var string_index: usize = 0;
3126 while (bit_it.next()) |bit_index| {
3127 const token = self.strings.items[string_index];
3128 std.debug.print("{}: [{}] {any}\n", .{ bit_index, string_index, token });
3129 string_index += 1;
3130 }
3131 }
3132
3133 pub fn applyAttributes(self: *Block, string_table: *Node.StringTable, source: []const u8, code_page_lookup: *const CodePageLookup) void {
3134 Compiler.applyToMemoryFlags(&self.memory_flags, string_table.common_resource_attributes, source);
3135 var dummy_language: res.Language = undefined;
3136 Compiler.applyToOptionalStatements(&dummy_language, &self.version, &self.characteristics, string_table.optional_statements, source, code_page_lookup);
3137 }
3138
3139 fn trimToDoubleNUL(comptime T: type, str: []const T) []const T {
3140 var last_was_null = false;
3141 for (str, 0..) |c, i| {
3142 if (c == 0) {
3143 if (last_was_null) return str[0 .. i - 1];
3144 last_was_null = true;
3145 } else {
3146 last_was_null = false;
3147 }
3148 }
3149 return str;
3150 }
3151
3152 test "trimToDoubleNUL" {
3153 try std.testing.expectEqualStrings("a\x00b", trimToDoubleNUL(u8, "a\x00b"));
3154 try std.testing.expectEqualStrings("a", trimToDoubleNUL(u8, "a\x00\x00b"));
3155 }
3156
3157 pub fn writeResData(self: *Block, compiler: *Compiler, language: res.Language, block_id: u16, writer: anytype) !void {
3158 var data_buffer = std.ArrayList(u8).init(compiler.allocator);
3159 defer data_buffer.deinit();
3160 const data_writer = data_buffer.writer();
3161
3162 var i: u8 = 0;
3163 var string_i: u8 = 0;
3164 while (true) : (i += 1) {
3165 if (!self.set_indexes.isSet(i)) {
3166 try data_writer.writeIntLittle(u16, 0);
3167 if (i == 15) break else continue;
3168 }
3169
3170 const string_token = self.strings.items[string_i];
3171 const slice = string_token.slice(compiler.source);
3172 const column = string_token.calculateColumn(compiler.source, 8, null);
3173 const code_page = compiler.input_code_pages.getForToken(string_token);
3174 const bytes = SourceBytes{ .slice = slice, .code_page = code_page };
3175 const utf16_string = try literals.parseQuotedStringAsWideString(compiler.allocator, bytes, .{
3176 .start_column = column,
3177 .diagnostics = .{ .diagnostics = compiler.diagnostics, .token = string_token },
3178 });
3179 defer compiler.allocator.free(utf16_string);
3180
3181 const trimmed_string = trim: {
3182 // Two NUL characters in a row act as a terminator
3183 // Note: This is only the case for STRINGTABLE strings
3184 var trimmed = trimToDoubleNUL(u16, utf16_string);
3185 // We also want to trim any trailing NUL characters
3186 break :trim std.mem.trimRight(u16, trimmed, &[_]u16{0});
3187 };
3188
3189 // String literals are limited to maxInt(u15) codepoints, so these UTF-16 encoded
3190 // strings are limited to maxInt(u15) * 2 = 65,534 code units (since 2 is the
3191 // maximum number of UTF-16 code units per codepoint).
3192 // This leaves room for exactly one NUL terminator.
3193 var string_len_in_utf16_code_units: u16 = @intCast(trimmed_string.len);
3194 // If the option is set, then a NUL terminator is added unconditionally.
3195 // We already trimmed any trailing NULs, so we know it will be a new addition to the string.
3196 if (compiler.null_terminate_string_table_strings) string_len_in_utf16_code_units += 1;
3197 try data_writer.writeIntLittle(u16, string_len_in_utf16_code_units);
3198 for (trimmed_string) |wc| {
3199 try data_writer.writeIntLittle(u16, wc);
3200 }
3201 if (compiler.null_terminate_string_table_strings) {
3202 try data_writer.writeIntLittle(u16, 0);
3203 }
3204
3205 if (i == 15) break;
3206 string_i += 1;
3207 }
3208
3209 // This intCast will never be able to fail due to the length constraints on string literals.
3210 //
3211 // - STRINGTABLE resource definitions can can only provide one string literal per index.
3212 // - STRINGTABLE strings are limited to maxInt(u16) UTF-16 code units (see 'string_len_in_utf16_code_units'
3213 // above), which means that the maximum number of bytes per string literal is
3214 // 2 * maxInt(u16) = 131,070 (since there are 2 bytes per UTF-16 code unit).
3215 // - Each Block/RT_STRING resource includes exactly 16 strings and each have a 2 byte
3216 // length field, so the maximum number of total bytes in a RT_STRING resource's data is
3217 // 16 * (131,070 + 2) = 2,097,152 which is well within the u32 max.
3218 //
3219 // Note: The string literal maximum length is enforced by the lexer.
3220 const data_size: u32 = @intCast(data_buffer.items.len);
3221
3222 const header = Compiler.ResourceHeader{
3223 .name_value = .{ .ordinal = block_id },
3224 .type_value = .{ .ordinal = @intFromEnum(res.RT.STRING) },
3225 .memory_flags = self.memory_flags,
3226 .language = language,
3227 .version = self.version,
3228 .characteristics = self.characteristics,
3229 .data_size = data_size,
3230 };
3231 // The only variable parts of the header are name and type, which in this case
3232 // we fully control and know are numbers, so they have a fixed size.
3233 try header.writeAssertNoOverflow(writer);
3234
3235 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
3236 try Compiler.writeResourceData(writer, data_fbs.reader(), data_size);
3237 }
3238 };
3239
3240 pub fn deinit(self: *StringTable, allocator: Allocator) void {
3241 var it = self.blocks.iterator();
3242 while (it.next()) |entry| {
3243 entry.value_ptr.strings.deinit(allocator);
3244 }
3245 self.blocks.deinit(allocator);
3246 }
3247
3248 const SetError = error{StringAlreadyDefined} || Allocator.Error;
3249
3250 pub fn set(
3251 self: *StringTable,
3252 allocator: Allocator,
3253 id: u16,
3254 string_token: Token,
3255 node: *Node,
3256 source: []const u8,
3257 code_page_lookup: *const CodePageLookup,
3258 version: u32,
3259 characteristics: u32,
3260 ) SetError!void {
3261 const block_id = (id / 16) + 1;
3262 const string_index: u8 = @intCast(id & 0xF);
3263
3264 var get_or_put_result = try self.blocks.getOrPut(allocator, block_id);
3265 if (!get_or_put_result.found_existing) {
3266 get_or_put_result.value_ptr.* = Block{ .version = version, .characteristics = characteristics };
3267 get_or_put_result.value_ptr.applyAttributes(node.cast(.string_table).?, source, code_page_lookup);
3268 } else {
3269 if (get_or_put_result.value_ptr.set_indexes.isSet(string_index)) {
3270 return error.StringAlreadyDefined;
3271 }
3272 }
3273
3274 var block = get_or_put_result.value_ptr;
3275 if (block.getInsertionIndex(string_index)) |insertion_index| {
3276 try block.strings.insert(allocator, insertion_index, string_token);
3277 } else {
3278 try block.strings.append(allocator, string_token);
3279 }
3280 block.set_indexes.set(string_index);
3281 }
3282
3283 pub fn get(self: *StringTable, id: u16) ?Token {
3284 const block_id = (id / 16) + 1;
3285 const string_index: u8 = @intCast(id & 0xF);
3286
3287 const block = self.blocks.getPtr(block_id) orelse return null;
3288 const token_index = block.getTokenIndex(string_index) orelse return null;
3289 return block.strings.items[token_index];
3290 }
3291
3292 pub fn dump(self: *StringTable) !void {
3293 var it = self.iterator();
3294 while (it.next()) |entry| {
3295 std.debug.print("block: {}\n", .{entry.key_ptr.*});
3296 entry.value_ptr.dump();
3297 }
3298 }
3299};
3300
3301test "StringTable" {
3302 const S = struct {
3303 fn makeDummyToken(id: usize) Token {
3304 return Token{
3305 .id = .invalid,
3306 .start = id,
3307 .end = id,
3308 .line_number = id,
3309 };
3310 }
3311 };
3312 const allocator = std.testing.allocator;
3313 var string_table = StringTable{};
3314 defer string_table.deinit(allocator);
3315
3316 var code_page_lookup = CodePageLookup.init(allocator, .windows1252);
3317 defer code_page_lookup.deinit();
3318
3319 var dummy_node = Node.StringTable{
3320 .type = S.makeDummyToken(0),
3321 .common_resource_attributes = &.{},
3322 .optional_statements = &.{},
3323 .begin_token = S.makeDummyToken(0),
3324 .strings = &.{},
3325 .end_token = S.makeDummyToken(0),
3326 };
3327
3328 // randomize an array of ids 0-99
3329 var ids = ids: {
3330 var buf: [100]u16 = undefined;
3331 var i: u16 = 0;
3332 while (i < buf.len) : (i += 1) {
3333 buf[i] = i;
3334 }
3335 break :ids buf;
3336 };
3337 var prng = std.rand.DefaultPrng.init(0);
3338 var random = prng.random();
3339 random.shuffle(u16, &ids);
3340
3341 // set each one in the randomized order
3342 for (ids) |id| {
3343 try string_table.set(allocator, id, S.makeDummyToken(id), &dummy_node.base, "", &code_page_lookup, 0, 0);
3344 }
3345
3346 // make sure each one exists and is the right value when gotten
3347 var id: u16 = 0;
3348 while (id < 100) : (id += 1) {
3349 const dummy = S.makeDummyToken(id);
3350 try std.testing.expectError(error.StringAlreadyDefined, string_table.set(allocator, id, dummy, &dummy_node.base, "", &code_page_lookup, 0, 0));
3351 try std.testing.expectEqual(dummy, string_table.get(id).?);
3352 }
3353
3354 // make sure non-existent string ids are not found
3355 try std.testing.expectEqual(@as(?Token, null), string_table.get(100));
3356}
src/resinator/errors.zig created+1033
......@@ -0,0 +1,1033 @@
1const std = @import("std");
2const Token = @import("lex.zig").Token;
3const SourceMappings = @import("source_mapping.zig").SourceMappings;
4const utils = @import("utils.zig");
5const rc = @import("rc.zig");
6const res = @import("res.zig");
7const ico = @import("ico.zig");
8const bmp = @import("bmp.zig");
9const parse = @import("parse.zig");
10const CodePage = @import("code_pages.zig").CodePage;
11
12pub const Diagnostics = struct {
13 errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
14 /// Append-only, cannot handle removing strings.
15 /// Expects to own all strings within the list.
16 strings: std.ArrayListUnmanaged([]const u8) = .{},
17 allocator: std.mem.Allocator,
18
19 pub fn init(allocator: std.mem.Allocator) Diagnostics {
20 return .{
21 .allocator = allocator,
22 };
23 }
24
25 pub fn deinit(self: *Diagnostics) void {
26 self.errors.deinit(self.allocator);
27 for (self.strings.items) |str| {
28 self.allocator.free(str);
29 }
30 self.strings.deinit(self.allocator);
31 }
32
33 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
34 try self.errors.append(self.allocator, error_details);
35 }
36
37 const SmallestStringIndexType = std.meta.Int(.unsigned, @min(
38 @bitSizeOf(ErrorDetails.FileOpenError.FilenameStringIndex),
39 @min(
40 @bitSizeOf(ErrorDetails.IconReadError.FilenameStringIndex),
41 @bitSizeOf(ErrorDetails.BitmapReadError.FilenameStringIndex),
42 ),
43 ));
44
45 /// Returns the index of the added string as the SmallestStringIndexType
46 /// in order to avoid needing to `@intCast` it at callsites of putString.
47 /// Instead, this function will error if the index would ever exceed the
48 /// smallest FilenameStringIndex of an ErrorDetails type.
49 pub fn putString(self: *Diagnostics, str: []const u8) !SmallestStringIndexType {
50 if (self.strings.items.len >= std.math.maxInt(SmallestStringIndexType)) {
51 return error.OutOfMemory; // ran out of string indexes
52 }
53 const dupe = try self.allocator.dupe(u8, str);
54 const index = self.strings.items.len;
55 try self.strings.append(self.allocator, dupe);
56 return @intCast(index);
57 }
58
59 pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.io.tty.Config, source_mappings: ?SourceMappings) void {
60 std.debug.getStderrMutex().lock();
61 defer std.debug.getStderrMutex().unlock();
62 const stderr = std.io.getStdErr().writer();
63 for (self.errors.items) |err_details| {
64 renderErrorMessage(self.allocator, stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return;
65 }
66 }
67
68 pub fn renderToStdErrDetectTTY(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void {
69 const tty_config = std.io.tty.detectConfig(std.io.getStdErr());
70 return self.renderToStdErr(cwd, source, tty_config, source_mappings);
71 }
72
73 pub fn contains(self: *const Diagnostics, err: ErrorDetails.Error) bool {
74 for (self.errors.items) |details| {
75 if (details.err == err) return true;
76 }
77 return false;
78 }
79
80 pub fn containsAny(self: *const Diagnostics, errors: []const ErrorDetails.Error) bool {
81 for (self.errors.items) |details| {
82 for (errors) |err| {
83 if (details.err == err) return true;
84 }
85 }
86 return false;
87 }
88};
89
90/// Contains enough context to append errors/warnings/notes etc
91pub const DiagnosticsContext = struct {
92 diagnostics: *Diagnostics,
93 token: Token,
94};
95
96pub const ErrorDetails = struct {
97 err: Error,
98 token: Token,
99 /// If non-null, should be before `token`. If null, `token` is assumed to be the start.
100 token_span_start: ?Token = null,
101 /// If non-null, should be after `token`. If null, `token` is assumed to be the end.
102 token_span_end: ?Token = null,
103 type: Type = .err,
104 print_source_line: bool = true,
105 extra: union {
106 none: void,
107 expected: Token.Id,
108 number: u32,
109 expected_types: ExpectedTypes,
110 resource: rc.Resource,
111 string_and_language: StringAndLanguage,
112 file_open_error: FileOpenError,
113 icon_read_error: IconReadError,
114 icon_dir: IconDirContext,
115 bmp_read_error: BitmapReadError,
116 accelerator_error: AcceleratorError,
117 statement_with_u16_param: StatementWithU16Param,
118 menu_or_class: enum { class, menu },
119 } = .{ .none = {} },
120
121 pub const Type = enum {
122 /// Fatal error, stops compilation
123 err,
124 /// Warning that does not affect compilation result
125 warning,
126 /// A note that typically provides further context for a warning/error
127 note,
128 /// An invisible diagnostic that is not printed to stderr but can
129 /// provide information useful when comparing the behavior of different
130 /// implementations. For example, a hint is emitted when a FONTDIR resource
131 /// was included in the .RES file which is significant because rc.exe
132 /// does something different than us, but ultimately it's not important
133 /// enough to be a warning/note.
134 hint,
135 };
136
137 comptime {
138 // all fields in the extra union should be 32 bits or less
139 for (std.meta.fields(std.meta.fieldInfo(ErrorDetails, .extra).type)) |field| {
140 std.debug.assert(@bitSizeOf(field.type) <= 32);
141 }
142 }
143
144 pub const StatementWithU16Param = enum(u32) {
145 fileversion,
146 productversion,
147 language,
148 };
149
150 pub const StringAndLanguage = packed struct(u32) {
151 id: u16,
152 language: res.Language,
153 };
154
155 pub const FileOpenError = packed struct(u32) {
156 err: FileOpenErrorEnum,
157 filename_string_index: FilenameStringIndex,
158
159 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum));
160 pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError);
161
162 pub fn enumFromError(err: std.fs.File.OpenError) FileOpenErrorEnum {
163 return switch (err) {
164 inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)),
165 };
166 }
167 };
168
169 pub const IconReadError = packed struct(u32) {
170 err: IconReadErrorEnum,
171 icon_type: enum(u1) { cursor, icon },
172 filename_string_index: FilenameStringIndex,
173
174 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(IconReadErrorEnum) - 1);
175 pub const IconReadErrorEnum = std.meta.FieldEnum(ico.ReadError);
176
177 pub fn enumFromError(err: ico.ReadError) IconReadErrorEnum {
178 return switch (err) {
179 inline else => |e| @field(ErrorDetails.IconReadError.IconReadErrorEnum, @errorName(e)),
180 };
181 }
182 };
183
184 pub const IconDirContext = packed struct(u32) {
185 icon_type: enum(u1) { cursor, icon },
186 icon_format: ico.ImageFormat,
187 index: u16,
188 bitmap_version: ico.BitmapHeader.Version = .unknown,
189 _: Padding = 0,
190
191 pub const Padding = std.meta.Int(.unsigned, 15 - @bitSizeOf(ico.BitmapHeader.Version) - @bitSizeOf(ico.ImageFormat));
192 };
193
194 pub const BitmapReadError = packed struct(u32) {
195 err: BitmapReadErrorEnum,
196 filename_string_index: FilenameStringIndex,
197
198 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(BitmapReadErrorEnum));
199 pub const BitmapReadErrorEnum = std.meta.FieldEnum(bmp.ReadError);
200
201 pub fn enumFromError(err: bmp.ReadError) BitmapReadErrorEnum {
202 return switch (err) {
203 inline else => |e| @field(ErrorDetails.BitmapReadError.BitmapReadErrorEnum, @errorName(e)),
204 };
205 }
206 };
207
208 pub const BitmapUnsupportedDIB = packed struct(u32) {
209 dib_version: ico.BitmapHeader.Version,
210 filename_string_index: FilenameStringIndex,
211
212 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(ico.BitmapHeader.Version));
213 };
214
215 pub const AcceleratorError = packed struct(u32) {
216 err: AcceleratorErrorEnum,
217 _: Padding = 0,
218
219 pub const Padding = std.meta.Int(.unsigned, 32 - @bitSizeOf(AcceleratorErrorEnum));
220 pub const AcceleratorErrorEnum = std.meta.FieldEnum(res.ParseAcceleratorKeyStringError);
221
222 pub fn enumFromError(err: res.ParseAcceleratorKeyStringError) AcceleratorErrorEnum {
223 return switch (err) {
224 inline else => |e| @field(ErrorDetails.AcceleratorError.AcceleratorErrorEnum, @errorName(e)),
225 };
226 }
227 };
228
229 pub const ExpectedTypes = packed struct(u32) {
230 number: bool = false,
231 number_expression: bool = false,
232 string_literal: bool = false,
233 accelerator_type_or_option: bool = false,
234 control_class: bool = false,
235 literal: bool = false,
236 // Note: This being 0 instead of undefined is arbitrary and something of a workaround,
237 // see https://github.com/ziglang/zig/issues/15395
238 _: u26 = 0,
239
240 pub const strings = std.ComptimeStringMap([]const u8, .{
241 .{ "number", "number" },
242 .{ "number_expression", "number expression" },
243 .{ "string_literal", "quoted string literal" },
244 .{ "accelerator_type_or_option", "accelerator type or option [ASCII, VIRTKEY, etc]" },
245 .{ "control_class", "control class [BUTTON, EDIT, etc]" },
246 .{ "literal", "unquoted literal" },
247 });
248
249 pub fn writeCommaSeparated(self: ExpectedTypes, writer: anytype) !void {
250 const struct_info = @typeInfo(ExpectedTypes).Struct;
251 const num_real_fields = struct_info.fields.len - 1;
252 const num_padding_bits = @bitSizeOf(ExpectedTypes) - num_real_fields;
253 const mask = std.math.maxInt(struct_info.backing_integer.?) >> num_padding_bits;
254 const relevant_bits_only = @as(struct_info.backing_integer.?, @bitCast(self)) & mask;
255 const num_set_bits = @popCount(relevant_bits_only);
256
257 var i: usize = 0;
258 inline for (struct_info.fields) |field_info| {
259 if (field_info.type != bool) continue;
260 if (i == num_set_bits) return;
261 if (@field(self, field_info.name)) {
262 try writer.writeAll(strings.get(field_info.name).?);
263 i += 1;
264 if (num_set_bits > 2 and i != num_set_bits) {
265 try writer.writeAll(", ");
266 } else if (i != num_set_bits) {
267 try writer.writeByte(' ');
268 }
269 if (num_set_bits > 1 and i == num_set_bits - 1) {
270 try writer.writeAll("or ");
271 }
272 }
273 }
274 }
275 };
276
277 pub const Error = enum {
278 // Lexer
279 unfinished_string_literal,
280 string_literal_too_long,
281 invalid_number_with_exponent,
282 invalid_digit_character_in_number_literal,
283 illegal_byte,
284 illegal_byte_outside_string_literals,
285 illegal_codepoint_outside_string_literals,
286 illegal_byte_order_mark,
287 illegal_private_use_character,
288 found_c_style_escaped_quote,
289 code_page_pragma_missing_left_paren,
290 code_page_pragma_missing_right_paren,
291 code_page_pragma_invalid_code_page,
292 code_page_pragma_not_integer,
293 code_page_pragma_overflow,
294 code_page_pragma_unsupported_code_page,
295
296 // Parser
297 unfinished_raw_data_block,
298 unfinished_string_table_block,
299 /// `expected` is populated.
300 expected_token,
301 /// `expected_types` is populated
302 expected_something_else,
303 /// `resource` is populated
304 resource_type_cant_use_raw_data,
305 /// `resource` is populated
306 id_must_be_ordinal,
307 /// `resource` is populated
308 name_or_id_not_allowed,
309 string_resource_as_numeric_type,
310 ascii_character_not_equivalent_to_virtual_key_code,
311 empty_menu_not_allowed,
312 rc_would_miscompile_version_value_padding,
313 rc_would_miscompile_version_value_byte_count,
314 code_page_pragma_in_included_file,
315 nested_resource_level_exceeds_max,
316 too_many_dialog_controls,
317 nested_expression_level_exceeds_max,
318 close_paren_expression,
319 unary_plus_expression,
320 rc_could_miscompile_control_params,
321
322 // Compiler
323 /// `string_and_language` is populated
324 string_already_defined,
325 font_id_already_defined,
326 /// `file_open_error` is populated
327 file_open_error,
328 /// `accelerator_error` is populated
329 invalid_accelerator_key,
330 accelerator_type_required,
331 rc_would_miscompile_control_padding,
332 rc_would_miscompile_control_class_ordinal,
333 /// `icon_dir` is populated
334 rc_would_error_on_icon_dir,
335 /// `icon_dir` is populated
336 format_not_supported_in_icon_dir,
337 /// `resource` is populated and contains the expected type
338 icon_dir_and_resource_type_mismatch,
339 /// `icon_read_error` is populated
340 icon_read_error,
341 /// `icon_dir` is populated
342 rc_would_error_on_bitmap_version,
343 /// `icon_dir` is populated
344 max_icon_ids_exhausted,
345 /// `bmp_read_error` is populated
346 bmp_read_error,
347 /// `number` is populated and contains a string index for which the string contains
348 /// the bytes of a `u64` (native endian). The `u64` contains the number of ignored bytes.
349 bmp_ignored_palette_bytes,
350 /// `number` is populated and contains a string index for which the string contains
351 /// the bytes of a `u64` (native endian). The `u64` contains the number of missing bytes.
352 bmp_missing_palette_bytes,
353 /// `number` is populated and contains a string index for which the string contains
354 /// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.
355 rc_would_miscompile_bmp_palette_padding,
356 /// `number` is populated and contains a string index for which the string contains
357 /// the bytes of two `u64`s (native endian). The first contains the number of missing
358 /// palette bytes and the second contains the max number of missing palette bytes.
359 /// If type is `.note`, then `extra` is `none`.
360 bmp_too_many_missing_palette_bytes,
361 resource_header_size_exceeds_max,
362 resource_data_size_exceeds_max,
363 control_extra_data_size_exceeds_max,
364 version_node_size_exceeds_max,
365 fontdir_size_exceeds_max,
366 /// `number` is populated and contains a string index for the filename
367 number_expression_as_filename,
368 /// `number` is populated and contains the control ID that is a duplicate
369 control_id_already_defined,
370 /// `number` is populated and contains the disallowed codepoint
371 invalid_filename,
372 /// `statement_with_u16_param` is populated
373 rc_would_error_u16_with_l_suffix,
374 result_contains_fontdir,
375 /// `number` is populated and contains the ordinal value that the id would be miscompiled to
376 rc_would_miscompile_dialog_menu_id,
377 /// `number` is populated and contains the ordinal value that the value would be miscompiled to
378 rc_would_miscompile_dialog_class,
379 /// `menu_or_class` is populated and contains the type of the parameter statement
380 rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
381 rc_would_miscompile_dialog_menu_id_starts_with_digit,
382 dialog_menu_id_was_uppercased,
383 /// `menu_or_class` is populated and contains the type of the parameter statement
384 duplicate_menu_or_class_skipped,
385 invalid_digit_character_in_ordinal,
386
387 // Literals
388 /// `number` is populated
389 rc_would_miscompile_codepoint_byte_swap,
390 /// `number` is populated
391 rc_would_miscompile_codepoint_skip,
392 tab_converted_to_spaces,
393
394 // General (used in various places)
395 /// `number` is populated and contains the value that the ordinal would have in the Win32 RC compiler implementation
396 win32_non_ascii_ordinal,
397 };
398
399 pub fn render(self: ErrorDetails, writer: anytype, source: []const u8, strings: []const []const u8) !void {
400 switch (self.err) {
401 .unfinished_string_literal => {
402 return writer.print("unfinished string literal at '{s}', expected closing '\"'", .{self.token.nameForErrorDisplay(source)});
403 },
404 .string_literal_too_long => {
405 return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number});
406 },
407 .invalid_number_with_exponent => {
408 return writer.print("base 10 number literal with exponent is not allowed: {s}", .{self.token.slice(source)});
409 },
410 .invalid_digit_character_in_number_literal => switch (self.type) {
411 .err, .warning => return writer.writeAll("non-ASCII digit characters are not allowed in number literals"),
412 .note => return writer.writeAll("the Win32 RC compiler allows non-ASCII digit characters, but will miscompile them"),
413 .hint => return,
414 },
415 .illegal_byte => {
416 return writer.print("character '{s}' is not allowed", .{std.fmt.fmtSliceEscapeUpper(self.token.slice(source))});
417 },
418 .illegal_byte_outside_string_literals => {
419 return writer.print("character '{s}' is not allowed outside of string literals", .{std.fmt.fmtSliceEscapeUpper(self.token.slice(source))});
420 },
421 .illegal_codepoint_outside_string_literals => {
422 // This is somewhat hacky, but we know that:
423 // - This error is only possible with codepoints outside of the Windows-1252 character range
424 // - So, the only supported code page that could generate this error is UTF-8
425 // Therefore, we just assume the token bytes are UTF-8 and decode them to get the illegal
426 // codepoint.
427 //
428 // FIXME: Support other code pages if they become relevant
429 const bytes = self.token.slice(source);
430 const codepoint = std.unicode.utf8Decode(bytes) catch unreachable;
431 return writer.print("codepoint <U+{X:0>4}> is not allowed outside of string literals", .{codepoint});
432 },
433 .illegal_byte_order_mark => {
434 return writer.writeAll("byte order mark <U+FEFF> is not allowed");
435 },
436 .illegal_private_use_character => {
437 return writer.writeAll("private use character <U+E000> is not allowed");
438 },
439 .found_c_style_escaped_quote => {
440 return writer.writeAll("escaping quotes with \\\" is not allowed (use \"\" instead)");
441 },
442 .code_page_pragma_missing_left_paren => {
443 return writer.writeAll("expected left parenthesis after 'code_page' in #pragma code_page");
444 },
445 .code_page_pragma_missing_right_paren => {
446 return writer.writeAll("expected right parenthesis after '<number>' in #pragma code_page");
447 },
448 .code_page_pragma_invalid_code_page => {
449 return writer.writeAll("invalid or unknown code page in #pragma code_page");
450 },
451 .code_page_pragma_not_integer => {
452 return writer.writeAll("code page is not a valid integer in #pragma code_page");
453 },
454 .code_page_pragma_overflow => {
455 return writer.writeAll("code page too large in #pragma code_page");
456 },
457 .code_page_pragma_unsupported_code_page => {
458 // We know that the token slice is a well-formed #pragma code_page(N), so
459 // we can skip to the first ( and then get the number that follows
460 const token_slice = self.token.slice(source);
461 var number_start = std.mem.indexOfScalar(u8, token_slice, '(').? + 1;
462 while (std.ascii.isWhitespace(token_slice[number_start])) {
463 number_start += 1;
464 }
465 var number_slice = token_slice[number_start..number_start];
466 while (std.ascii.isDigit(token_slice[number_start + number_slice.len])) {
467 number_slice.len += 1;
468 }
469 const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable;
470 const code_page = CodePage.getByIdentifier(number) catch unreachable;
471 // TODO: Improve or maybe add a note making it more clear that the code page
472 // is valid and that the code page is unsupported purely due to a limitation
473 // in this compiler.
474 return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number });
475 },
476 .unfinished_raw_data_block => {
477 return writer.print("unfinished raw data block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});
478 },
479 .unfinished_string_table_block => {
480 return writer.print("unfinished STRINGTABLE block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});
481 },
482 .expected_token => {
483 return writer.print("expected '{s}', got '{s}'", .{ self.extra.expected.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });
484 },
485 .expected_something_else => {
486 try writer.writeAll("expected ");
487 try self.extra.expected_types.writeCommaSeparated(writer);
488 return writer.print("; got '{s}'", .{self.token.nameForErrorDisplay(source)});
489 },
490 .resource_type_cant_use_raw_data => switch (self.type) {
491 .err, .warning => try writer.print("expected '<filename>', found '{s}' (resource type '{s}' can't use raw data)", .{ self.token.nameForErrorDisplay(source), self.extra.resource.nameForErrorDisplay() }),
492 .note => try writer.print("if '{s}' is intended to be a filename, it must be specified as a quoted string literal", .{self.token.nameForErrorDisplay(source)}),
493 .hint => return,
494 },
495 .id_must_be_ordinal => {
496 try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{s}'", .{ self.extra.resource.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });
497 },
498 .name_or_id_not_allowed => {
499 try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()});
500 },
501 .string_resource_as_numeric_type => switch (self.type) {
502 .err, .warning => try writer.writeAll("the number 6 (RT_STRING) cannot be used as a resource type"),
503 .note => try writer.writeAll("using RT_STRING directly likely results in an invalid .res file, use a STRINGTABLE instead"),
504 .hint => return,
505 },
506 .ascii_character_not_equivalent_to_virtual_key_code => {
507 // TODO: Better wording? This is what the Win32 RC compiler emits.
508 // This occurs when VIRTKEY and a control code is specified ("^c", etc)
509 try writer.writeAll("ASCII character not equivalent to virtual key code");
510 },
511 .empty_menu_not_allowed => {
512 try writer.print("empty menu of type '{s}' not allowed", .{self.token.nameForErrorDisplay(source)});
513 },
514 .rc_would_miscompile_version_value_padding => switch (self.type) {
515 .err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}),
516 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma between the key and the quoted string", .{}),
517 .hint => return,
518 },
519 .rc_would_miscompile_version_value_byte_count => switch (self.type) {
520 .err, .warning => return writer.print("the byte count of this value would be miscompiled by the Win32 RC compiler", .{}),
521 .note => return writer.print("to avoid the potential miscompilation, do not mix numbers and strings within a value", .{}),
522 .hint => return,
523 },
524 .code_page_pragma_in_included_file => {
525 try writer.print("#pragma code_page is not supported in an included resource file", .{});
526 },
527 .nested_resource_level_exceeds_max => switch (self.type) {
528 .err, .warning => {
529 const max = switch (self.extra.resource) {
530 .versioninfo => parse.max_nested_version_level,
531 .menu, .menuex => parse.max_nested_menu_level,
532 else => unreachable,
533 };
534 return writer.print("{s} contains too many nested children (max is {})", .{ self.extra.resource.nameForErrorDisplay(), max });
535 },
536 .note => return writer.print("max {s} nesting level exceeded here", .{self.extra.resource.nameForErrorDisplay()}),
537 .hint => return,
538 },
539 .too_many_dialog_controls => switch (self.type) {
540 .err, .warning => return writer.print("{s} contains too many controls (max is {})", .{ self.extra.resource.nameForErrorDisplay(), std.math.maxInt(u16) }),
541 .note => return writer.writeAll("maximum number of controls exceeded here"),
542 .hint => return,
543 },
544 .nested_expression_level_exceeds_max => switch (self.type) {
545 .err, .warning => return writer.print("expression contains too many syntax levels (max is {})", .{parse.max_nested_expression_level}),
546 .note => return writer.print("maximum expression level exceeded here", .{}),
547 .hint => return,
548 },
549 .close_paren_expression => {
550 try writer.writeAll("the Win32 RC compiler would accept ')' as a valid expression, but it would be skipped over and potentially lead to unexpected outcomes");
551 },
552 .unary_plus_expression => {
553 try writer.writeAll("the Win32 RC compiler may accept '+' as a unary operator here, but it is not supported in this implementation; consider omitting the unary +");
554 },
555 .rc_could_miscompile_control_params => switch (self.type) {
556 .err, .warning => return writer.print("this token could be erroneously skipped over by the Win32 RC compiler", .{}),
557 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}),
558 .hint => return,
559 },
560 .string_already_defined => switch (self.type) {
561 // TODO: better printing of language, using constant names from WinNT.h
562 .err, .warning => return writer.print("string with id {d} (0x{X}) already defined for language {d},{d}", .{ self.extra.string_and_language.id, self.extra.string_and_language.id, self.extra.string_and_language.language.primary_language_id, self.extra.string_and_language.language.sublanguage_id }),
563 .note => return writer.print("previous definition of string with id {d} (0x{X}) here", .{ self.extra.string_and_language.id, self.extra.string_and_language.id }),
564 .hint => return,
565 },
566 .font_id_already_defined => switch (self.type) {
567 .err => return writer.print("font with id {d} already defined", .{self.extra.number}),
568 .warning => return writer.print("skipped duplicate font with id {d}", .{self.extra.number}),
569 .note => return writer.print("previous definition of font with id {d} here", .{self.extra.number}),
570 .hint => return,
571 },
572 .file_open_error => {
573 try writer.print("unable to open file '{s}': {s}", .{ strings[self.extra.file_open_error.filename_string_index], @tagName(self.extra.file_open_error.err) });
574 },
575 .invalid_accelerator_key => {
576 try writer.print("invalid accelerator key '{s}': {s}", .{ self.token.nameForErrorDisplay(source), @tagName(self.extra.accelerator_error.err) });
577 },
578 .accelerator_type_required => {
579 try writer.print("accelerator type [ASCII or VIRTKEY] required when key is an integer", .{});
580 },
581 .rc_would_miscompile_control_padding => switch (self.type) {
582 .err, .warning => return writer.print("the padding before this control would be miscompiled by the Win32 RC compiler (it would insert 2 extra bytes of padding)", .{}),
583 .note => return writer.print("to avoid the potential miscompilation, consider removing any 'control data' blocks from the controls in this dialog", .{}),
584 .hint => return,
585 },
586 .rc_would_miscompile_control_class_ordinal => switch (self.type) {
587 .err, .warning => return writer.print("the control class of this CONTROL would be miscompiled by the Win32 RC compiler", .{}),
588 .note => return writer.print("to avoid the potential miscompilation, consider specifying the control class using a string (BUTTON, EDIT, etc) instead of a number", .{}),
589 .hint => return,
590 },
591 .rc_would_error_on_icon_dir => switch (self.type) {
592 .err, .warning => return writer.print("the resource at index {} of this {s} has the format '{s}'; this would be an error in the Win32 RC compiler", .{ self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type), @tagName(self.extra.icon_dir.icon_format) }),
593 .note => {
594 // The only note supported is one specific to exactly this combination
595 if (!(self.extra.icon_dir.icon_type == .icon and self.extra.icon_dir.icon_format == .riff)) unreachable;
596 try writer.print("animated RIFF icons within resource groups may not be well supported, consider using an animated icon file (.ani) instead", .{});
597 },
598 .hint => return,
599 },
600 .format_not_supported_in_icon_dir => {
601 try writer.print("resource with format '{s}' (at index {}) is not allowed in {s} resource groups", .{ @tagName(self.extra.icon_dir.icon_format), self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) });
602 },
603 .icon_dir_and_resource_type_mismatch => {
604 const unexpected_type: rc.Resource = if (self.extra.resource == .icon) .cursor else .icon;
605 // TODO: Better wording
606 try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() });
607 },
608 .icon_read_error => {
609 try writer.print("unable to read {s} file '{s}': {s}", .{ @tagName(self.extra.icon_read_error.icon_type), strings[self.extra.icon_read_error.filename_string_index], @tagName(self.extra.icon_read_error.err) });
610 },
611 .rc_would_error_on_bitmap_version => switch (self.type) {
612 .err => try writer.print("the DIB at index {} of this {s} is of version '{s}'; this version is no longer allowed and should be upgraded to '{s}'", .{
613 self.extra.icon_dir.index,
614 @tagName(self.extra.icon_dir.icon_type),
615 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
616 ico.BitmapHeader.Version.@"nt3.1".nameForErrorDisplay(),
617 }),
618 .warning => try writer.print("the DIB at index {} of this {s} is of version '{s}'; this would be an error in the Win32 RC compiler", .{
619 self.extra.icon_dir.index,
620 @tagName(self.extra.icon_dir.icon_type),
621 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
622 }),
623 .note => unreachable,
624 .hint => return,
625 },
626 .max_icon_ids_exhausted => switch (self.type) {
627 .err, .warning => try writer.print("maximum global icon/cursor ids exhausted (max is {})", .{std.math.maxInt(u16) - 1}),
628 .note => try writer.print("maximum icon/cursor id exceeded at index {} of this {s}", .{ self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) }),
629 .hint => return,
630 },
631 .bmp_read_error => {
632 try writer.print("invalid bitmap file '{s}': {s}", .{ strings[self.extra.bmp_read_error.filename_string_index], @tagName(self.extra.bmp_read_error.err) });
633 },
634 .bmp_ignored_palette_bytes => {
635 const bytes = strings[self.extra.number];
636 const ignored_bytes = std.mem.readIntNative(u64, bytes[0..8]);
637 try writer.print("bitmap has {d} extra bytes preceding the pixel data which will be ignored", .{ignored_bytes});
638 },
639 .bmp_missing_palette_bytes => {
640 const bytes = strings[self.extra.number];
641 const missing_bytes = std.mem.readIntNative(u64, bytes[0..8]);
642 try writer.print("bitmap has {d} missing color palette bytes which will be padded with zeroes", .{missing_bytes});
643 },
644 .rc_would_miscompile_bmp_palette_padding => {
645 const bytes = strings[self.extra.number];
646 const miscompiled_bytes = std.mem.readIntNative(u64, bytes[0..8]);
647 try writer.print("the missing color palette bytes would be miscompiled by the Win32 RC compiler (the added padding bytes would include {d} bytes of the pixel data)", .{miscompiled_bytes});
648 },
649 .bmp_too_many_missing_palette_bytes => switch (self.type) {
650 .err, .warning => {
651 const bytes = strings[self.extra.number];
652 const missing_bytes = std.mem.readIntNative(u64, bytes[0..8]);
653 const max_missing_bytes = std.mem.readIntNative(u64, bytes[8..16]);
654 try writer.print("bitmap has {} missing color palette bytes which exceeds the maximum of {}", .{ missing_bytes, max_missing_bytes });
655 },
656 // TODO: command line option
657 .note => try writer.writeAll("the maximum number of missing color palette bytes is configurable via <<TODO command line option>>"),
658 .hint => return,
659 },
660 .resource_header_size_exceeds_max => {
661 try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)});
662 },
663 .resource_data_size_exceeds_max => switch (self.type) {
664 .err, .warning => return writer.print("resource's data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
665 .note => return writer.print("maximum data length exceeded here", .{}),
666 .hint => return,
667 },
668 .control_extra_data_size_exceeds_max => switch (self.type) {
669 .err, .warning => try writer.print("control data length exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
670 .note => return writer.print("maximum control data length exceeded here", .{}),
671 .hint => return,
672 },
673 .version_node_size_exceeds_max => switch (self.type) {
674 .err, .warning => return writer.print("version node tree size exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
675 .note => return writer.print("maximum tree size exceeded while writing this child", .{}),
676 .hint => return,
677 },
678 .fontdir_size_exceeds_max => switch (self.type) {
679 .err, .warning => return writer.print("FONTDIR data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
680 .note => return writer.writeAll("this is likely due to the size of the combined lengths of the device/face names of all FONT resources"),
681 .hint => return,
682 },
683 .number_expression_as_filename => switch (self.type) {
684 .err, .warning => return writer.writeAll("filename cannot be specified using a number expression, consider using a quoted string instead"),
685 .note => return writer.print("the Win32 RC compiler would evaluate this number expression as the filename '{s}'", .{strings[self.extra.number]}),
686 .hint => return,
687 },
688 .control_id_already_defined => switch (self.type) {
689 .err, .warning => return writer.print("control with id {d} already defined for this dialog", .{self.extra.number}),
690 .note => return writer.print("previous definition of control with id {d} here", .{self.extra.number}),
691 .hint => return,
692 },
693 .invalid_filename => {
694 const disallowed_codepoint = self.extra.number;
695 if (disallowed_codepoint < 128 and std.ascii.isPrint(@intCast(disallowed_codepoint))) {
696 try writer.print("evaluated filename contains a disallowed character: '{c}'", .{@as(u8, @intCast(disallowed_codepoint))});
697 } else {
698 try writer.print("evaluated filename contains a disallowed codepoint: <U+{X:0>4}>", .{disallowed_codepoint});
699 }
700 },
701 .rc_would_error_u16_with_l_suffix => switch (self.type) {
702 .err, .warning => return writer.print("this {s} parameter would be an error in the Win32 RC compiler", .{@tagName(self.extra.statement_with_u16_param)}),
703 .note => return writer.writeAll("to avoid the error, remove any L suffixes from numbers within the parameter"),
704 .hint => return,
705 },
706 .result_contains_fontdir => return,
707 .rc_would_miscompile_dialog_menu_id => switch (self.type) {
708 .err, .warning => return writer.print("the id of this menu would be miscompiled by the Win32 RC compiler", .{}),
709 .note => return writer.print("the Win32 RC compiler would evaluate the id as the ordinal/number value {d}", .{self.extra.number}),
710 .hint => return,
711 },
712 .rc_would_miscompile_dialog_class => switch (self.type) {
713 .err, .warning => return writer.print("this class would be miscompiled by the Win32 RC compiler", .{}),
714 .note => return writer.print("the Win32 RC compiler would evaluate it as the ordinal/number value {d}", .{self.extra.number}),
715 .hint => return,
716 },
717 .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal => switch (self.type) {
718 .err, .warning => return,
719 .note => return writer.print("to avoid the potential miscompilation, only specify one {s} per dialog resource", .{@tagName(self.extra.menu_or_class)}),
720 .hint => return,
721 },
722 .rc_would_miscompile_dialog_menu_id_starts_with_digit => switch (self.type) {
723 .err, .warning => return,
724 .note => return writer.writeAll("to avoid the potential miscompilation, the first character of the id should not be a digit"),
725 .hint => return,
726 },
727 .dialog_menu_id_was_uppercased => return,
728 .duplicate_menu_or_class_skipped => {
729 return writer.print("this {s} was ignored; when multiple {s} statements are specified, only the last takes precedence", .{
730 @tagName(self.extra.menu_or_class),
731 @tagName(self.extra.menu_or_class),
732 });
733 },
734 .invalid_digit_character_in_ordinal => {
735 return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values");
736 },
737 .rc_would_miscompile_codepoint_byte_swap => switch (self.type) {
738 .err, .warning => return writer.print("codepoint U+{X} within a string literal would be miscompiled by the Win32 RC compiler (the bytes of the UTF-16 code unit would be swapped)", .{self.extra.number}),
739 .note => return writer.print("to avoid the potential miscompilation, an integer escape sequence in a wide string literal could be used instead: L\"\\x{X}\"", .{self.extra.number}),
740 .hint => return,
741 },
742 .rc_would_miscompile_codepoint_skip => switch (self.type) {
743 .err, .warning => return writer.print("codepoint U+{X} within a string literal would be miscompiled by the Win32 RC compiler (the codepoint would be missing from the compiled resource)", .{self.extra.number}),
744 .note => return writer.print("to avoid the potential miscompilation, an integer escape sequence in a wide string literal could be used instead: L\"\\x{X}\"", .{self.extra.number}),
745 .hint => return,
746 },
747 .tab_converted_to_spaces => switch (self.type) {
748 .err, .warning => return writer.writeAll("the tab character(s) in this string will be converted into a variable number of spaces (determined by the column of the tab character in the .rc file)"),
749 .note => return writer.writeAll("to include the tab character itself in a string, the escape sequence \\t should be used"),
750 .hint => return,
751 },
752 .win32_non_ascii_ordinal => switch (self.type) {
753 .err, .warning => unreachable,
754 .note => return writer.print("the Win32 RC compiler would accept this as an ordinal but its value would be {}", .{self.extra.number}),
755 .hint => return,
756 },
757 }
758 }
759
760 pub const VisualTokenInfo = struct {
761 before_len: usize,
762 point_offset: usize,
763 after_len: usize,
764 };
765
766 pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize) VisualTokenInfo {
767 // Note: A perfect solution here would involve full grapheme cluster
768 // awareness, but oh well. This will give incorrect offsets
769 // if there are any multibyte codepoints within the relevant span,
770 // and even more inflated for grapheme clusters.
771 //
772 // We mitigate this slightly when we know we'll be pointing at
773 // something that displays as 1 character.
774 return switch (self.err) {
775 // These can technically be more than 1 byte depending on encoding,
776 // but they always refer to one visual character/grapheme.
777 .illegal_byte,
778 .illegal_byte_outside_string_literals,
779 .illegal_codepoint_outside_string_literals,
780 .illegal_byte_order_mark,
781 .illegal_private_use_character,
782 => .{
783 .before_len = 0,
784 .point_offset = self.token.start - source_line_start,
785 .after_len = 0,
786 },
787 else => .{
788 .before_len = before: {
789 const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start);
790 break :before self.token.start - start;
791 },
792 .point_offset = self.token.start - source_line_start,
793 .after_len = after: {
794 const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end);
795 if (end == self.token.start) break :after 0;
796 break :after end - self.token.start - 1;
797 },
798 },
799 };
800 }
801};
802
803pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_config: std.io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void {
804 if (err_details.type == .hint) return;
805
806 const source_line_start = err_details.token.getLineStart(source);
807 const column = err_details.token.calculateColumn(source, 1, source_line_start);
808
809 // var counting_writer_container = std.io.countingWriter(writer);
810 // const counting_writer = counting_writer_container.writer();
811
812 const corresponding_span: ?SourceMappings.SourceSpan = if (source_mappings) |mappings| mappings.get(err_details.token.line_number) else null;
813 const corresponding_file: ?[]const u8 = if (source_mappings) |mappings| mappings.files.get(corresponding_span.?.filename_offset) else null;
814
815 const err_line = if (corresponding_span) |span| span.start_line else err_details.token.line_number;
816
817 try tty_config.setColor(writer, .bold);
818 if (corresponding_file) |file| {
819 try writer.writeAll(file);
820 } else {
821 try tty_config.setColor(writer, .dim);
822 try writer.writeAll("<after preprocessor>");
823 try tty_config.setColor(writer, .reset);
824 try tty_config.setColor(writer, .bold);
825 }
826 try writer.print(":{d}:{d}: ", .{ err_line, column });
827 switch (err_details.type) {
828 .err => {
829 try tty_config.setColor(writer, .red);
830 try writer.writeAll("error: ");
831 },
832 .warning => {
833 try tty_config.setColor(writer, .yellow);
834 try writer.writeAll("warning: ");
835 },
836 .note => {
837 try tty_config.setColor(writer, .cyan);
838 try writer.writeAll("note: ");
839 },
840 .hint => unreachable,
841 }
842 try tty_config.setColor(writer, .reset);
843 try tty_config.setColor(writer, .bold);
844 try err_details.render(writer, source, strings);
845 try writer.writeByte('\n');
846 try tty_config.setColor(writer, .reset);
847
848 if (!err_details.print_source_line) {
849 try writer.writeByte('\n');
850 return;
851 }
852
853 const source_line = err_details.token.getLine(source, source_line_start);
854 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
855
856 // Need this to determine if the 'line originated from' note is worth printing
857 var source_line_for_display_buf = try std.ArrayList(u8).initCapacity(allocator, source_line.len);
858 defer source_line_for_display_buf.deinit();
859 try writeSourceSlice(source_line_for_display_buf.writer(), source_line);
860
861 // TODO: General handling of long lines, not tied to this specific error
862 if (err_details.err == .string_literal_too_long) {
863 const before_slice = source_line[0..@min(source_line.len, visual_info.point_offset + 16)];
864 try writeSourceSlice(writer, before_slice);
865 try tty_config.setColor(writer, .dim);
866 try writer.writeAll("<...truncated...>");
867 try tty_config.setColor(writer, .reset);
868 } else {
869 try writer.writeAll(source_line_for_display_buf.items);
870 }
871 try writer.writeByte('\n');
872
873 try tty_config.setColor(writer, .green);
874 const num_spaces = visual_info.point_offset - visual_info.before_len;
875 try writer.writeByteNTimes(' ', num_spaces);
876 try writer.writeByteNTimes('~', visual_info.before_len);
877 try writer.writeByte('^');
878 if (visual_info.after_len > 0) {
879 var num_squiggles = visual_info.after_len;
880 if (err_details.err == .string_literal_too_long) {
881 num_squiggles = @min(num_squiggles, 15);
882 }
883 try writer.writeByteNTimes('~', num_squiggles);
884 }
885 try writer.writeByte('\n');
886 try tty_config.setColor(writer, .reset);
887
888 if (source_mappings) |_| {
889 var corresponding_lines = try CorrespondingLines.init(allocator, cwd, err_details, source_line_for_display_buf.items, corresponding_span.?, corresponding_file.?);
890 defer corresponding_lines.deinit(allocator);
891
892 if (!corresponding_lines.worth_printing_note) return;
893
894 try tty_config.setColor(writer, .bold);
895 if (corresponding_file) |file| {
896 try writer.writeAll(file);
897 } else {
898 try tty_config.setColor(writer, .dim);
899 try writer.writeAll("<after preprocessor>");
900 try tty_config.setColor(writer, .reset);
901 try tty_config.setColor(writer, .bold);
902 }
903 try writer.print(":{d}:{d}: ", .{ err_line, column });
904 try tty_config.setColor(writer, .cyan);
905 try writer.writeAll("note: ");
906 try tty_config.setColor(writer, .reset);
907 try tty_config.setColor(writer, .bold);
908 try writer.writeAll("this line originated from line");
909 if (corresponding_span.?.start_line != corresponding_span.?.end_line) {
910 try writer.print("s {}-{}", .{ corresponding_span.?.start_line, corresponding_span.?.end_line });
911 } else {
912 try writer.print(" {}", .{corresponding_span.?.start_line});
913 }
914 try writer.print(" of file '{s}'\n", .{corresponding_file.?});
915 try tty_config.setColor(writer, .reset);
916
917 if (!corresponding_lines.worth_printing_lines) return;
918
919 if (corresponding_lines.lines_is_error_message) {
920 try tty_config.setColor(writer, .red);
921 try writer.writeAll(" | ");
922 try tty_config.setColor(writer, .reset);
923 try tty_config.setColor(writer, .dim);
924 try writer.writeAll(corresponding_lines.lines.items);
925 try tty_config.setColor(writer, .reset);
926 try writer.writeAll("\n\n");
927 return;
928 }
929
930 try writer.writeAll(corresponding_lines.lines.items);
931 try writer.writeAll("\n\n");
932 }
933}
934
935const CorrespondingLines = struct {
936 worth_printing_note: bool = true,
937 worth_printing_lines: bool = true,
938 lines: std.ArrayListUnmanaged(u8) = .{},
939 lines_is_error_message: bool = false,
940
941 pub fn init(allocator: std.mem.Allocator, cwd: std.fs.Dir, err_details: ErrorDetails, lines_for_comparison: []const u8, corresponding_span: SourceMappings.SourceSpan, corresponding_file: []const u8) !CorrespondingLines {
942 var corresponding_lines = CorrespondingLines{};
943
944 // We don't do line comparison for this error, so don't print the note if the line
945 // number is different
946 if (err_details.err == .string_literal_too_long and err_details.token.line_number == corresponding_span.start_line) {
947 corresponding_lines.worth_printing_note = false;
948 return corresponding_lines;
949 }
950
951 // Don't print the originating line for this error, we know it's really long
952 if (err_details.err == .string_literal_too_long) {
953 corresponding_lines.worth_printing_lines = false;
954 return corresponding_lines;
955 }
956
957 var writer = corresponding_lines.lines.writer(allocator);
958 if (utils.openFileNotDir(cwd, corresponding_file, .{})) |file| {
959 defer file.close();
960 var buffered_reader = std.io.bufferedReader(file.reader());
961 writeLinesFromStream(writer, buffered_reader.reader(), corresponding_span.start_line, corresponding_span.end_line) catch |err| switch (err) {
962 error.LinesNotFound => {
963 corresponding_lines.lines.clearRetainingCapacity();
964 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});
965 corresponding_lines.lines_is_error_message = true;
966 return corresponding_lines;
967 },
968 else => |e| return e,
969 };
970 } else |err| {
971 corresponding_lines.lines.clearRetainingCapacity();
972 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});
973 corresponding_lines.lines_is_error_message = true;
974 return corresponding_lines;
975 }
976
977 // If the lines are the same as they were before preprocessing, skip printing the note entirely
978 if (std.mem.eql(u8, lines_for_comparison, corresponding_lines.lines.items)) {
979 corresponding_lines.worth_printing_note = false;
980 }
981 return corresponding_lines;
982 }
983
984 pub fn deinit(self: *CorrespondingLines, allocator: std.mem.Allocator) void {
985 self.lines.deinit(allocator);
986 }
987};
988
989fn writeSourceSlice(writer: anytype, slice: []const u8) !void {
990 for (slice) |c| try writeSourceByte(writer, c);
991}
992
993inline fn writeSourceByte(writer: anytype, byte: u8) !void {
994 switch (byte) {
995 '\x00'...'\x08', '\x0E'...'\x1F', '\x7F' => try writer.writeAll("�"),
996 // \r is seemingly ignored by the RC compiler so skipping it when printing source lines
997 // could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up
998 // in the console as DATA but the compiler reads it as RCDATA)
999 //
1000 // NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r
1001 // characters get converted to \n, but may become relevant if another
1002 // preprocessor is used instead.
1003 '\r' => {},
1004 '\t', '\x0B', '\x0C' => try writer.writeByte(' '),
1005 else => try writer.writeByte(byte),
1006 }
1007}
1008
1009pub fn writeLinesFromStream(writer: anytype, input: anytype, start_line: usize, end_line: usize) !void {
1010 var line_num: usize = 1;
1011 while (try readByteOrEof(input)) |byte| {
1012 switch (byte) {
1013 '\n' => {
1014 if (line_num == end_line) return;
1015 if (line_num >= start_line) try writeSourceByte(writer, byte);
1016 line_num += 1;
1017 },
1018 else => {
1019 if (line_num >= start_line) try writeSourceByte(writer, byte);
1020 },
1021 }
1022 }
1023 if (line_num != end_line) {
1024 return error.LinesNotFound;
1025 }
1026}
1027
1028pub fn readByteOrEof(reader: anytype) !?u8 {
1029 return reader.readByte() catch |err| switch (err) {
1030 error.EndOfStream => return null,
1031 else => |e| return e,
1032 };
1033}
src/resinator/ico.zig created+310
......@@ -0,0 +1,310 @@
1//! https://devblogs.microsoft.com/oldnewthing/20120720-00/?p=7083
2//! https://learn.microsoft.com/en-us/previous-versions/ms997538(v=msdn.10)
3//! https://learn.microsoft.com/en-us/windows/win32/menurc/newheader
4//! https://learn.microsoft.com/en-us/windows/win32/menurc/resdir
5//! https://learn.microsoft.com/en-us/windows/win32/menurc/localheader
6
7const std = @import("std");
8
9pub const ReadError = std.mem.Allocator.Error || error{ InvalidHeader, InvalidImageType, ImpossibleDataSize, UnexpectedEOF, ReadError };
10
11pub fn read(allocator: std.mem.Allocator, reader: anytype, max_size: u64) ReadError!IconDir {
12 // Some Reader implementations have an empty ReadError error set which would
13 // cause 'unreachable else' if we tried to use an else in the switch, so we
14 // need to detect this case and not try to translate to ReadError
15 const empty_reader_errorset = @typeInfo(@TypeOf(reader).Error).ErrorSet == null or @typeInfo(@TypeOf(reader).Error).ErrorSet.?.len == 0;
16 if (empty_reader_errorset) {
17 return readAnyError(allocator, reader, max_size) catch |err| switch (err) {
18 error.EndOfStream => error.UnexpectedEOF,
19 else => |e| return e,
20 };
21 } else {
22 return readAnyError(allocator, reader, max_size) catch |err| switch (err) {
23 error.OutOfMemory,
24 error.InvalidHeader,
25 error.InvalidImageType,
26 error.ImpossibleDataSize,
27 => |e| return e,
28 error.EndOfStream => error.UnexpectedEOF,
29 // The remaining errors are dependent on the `reader`, so
30 // we just translate them all to generic ReadError
31 else => error.ReadError,
32 };
33 }
34}
35
36// TODO: This seems like a somewhat strange pattern, could be a better way
37// to do this. Maybe it makes more sense to handle the translation
38// at the call site instead of having a helper function here.
39pub fn readAnyError(allocator: std.mem.Allocator, reader: anytype, max_size: u64) !IconDir {
40 const reserved = try reader.readIntLittle(u16);
41 if (reserved != 0) {
42 return error.InvalidHeader;
43 }
44
45 const image_type = reader.readEnum(ImageType, .Little) catch |err| switch (err) {
46 error.InvalidValue => return error.InvalidImageType,
47 else => |e| return e,
48 };
49
50 const num_images = try reader.readIntLittle(u16);
51
52 // To avoid over-allocation in the case of a file that says it has way more
53 // entries than it actually does, we use an ArrayList with a conservatively
54 // limited initial capacity instead of allocating the entire slice at once.
55 const initial_capacity = @min(num_images, 8);
56 var entries = try std.ArrayList(Entry).initCapacity(allocator, initial_capacity);
57 errdefer entries.deinit();
58
59 var i: usize = 0;
60 while (i < num_images) : (i += 1) {
61 var entry: Entry = undefined;
62 entry.width = try reader.readByte();
63 entry.height = try reader.readByte();
64 entry.num_colors = try reader.readByte();
65 entry.reserved = try reader.readByte();
66 switch (image_type) {
67 .icon => {
68 entry.type_specific_data = .{ .icon = .{
69 .color_planes = try reader.readIntLittle(u16),
70 .bits_per_pixel = try reader.readIntLittle(u16),
71 } };
72 },
73 .cursor => {
74 entry.type_specific_data = .{ .cursor = .{
75 .hotspot_x = try reader.readIntLittle(u16),
76 .hotspot_y = try reader.readIntLittle(u16),
77 } };
78 },
79 }
80 entry.data_size_in_bytes = try reader.readIntLittle(u32);
81 entry.data_offset_from_start_of_file = try reader.readIntLittle(u32);
82 // Validate that the offset/data size is feasible
83 if (@as(u64, entry.data_offset_from_start_of_file) + entry.data_size_in_bytes > max_size) {
84 return error.ImpossibleDataSize;
85 }
86 // and that the data size is large enough for at least the header of an image
87 // Note: This avoids needing to deal with a miscompilation from the Win32 RC
88 // compiler when the data size of an image is specified as zero but there
89 // is data to-be-read at the offset. The Win32 RC compiler will output
90 // an ICON/CURSOR resource with a bogus size in its header but with no actual
91 // data bytes in it, leading to an invalid .res. Similarly, if, for example,
92 // there is valid PNG data at the image's offset, but the size is specified
93 // as fewer bytes than the PNG header, then the Win32 RC compiler will still
94 // treat it as a PNG (e.g. unconditionally set num_planes to 1) but the data
95 // of the resource will only be 1 byte so treating it as a PNG doesn't make
96 // sense (especially not when you have to read past the data size to determine
97 // that it's a PNG).
98 if (entry.data_size_in_bytes < 16) {
99 return error.ImpossibleDataSize;
100 }
101 try entries.append(entry);
102 }
103
104 return .{
105 .image_type = image_type,
106 .entries = try entries.toOwnedSlice(),
107 .allocator = allocator,
108 };
109}
110
111pub const ImageType = enum(u16) {
112 icon = 1,
113 cursor = 2,
114};
115
116pub const IconDir = struct {
117 image_type: ImageType,
118 /// Note: entries.len will always fit into a u16, since the field containing the
119 /// number of images in an ico file is a u16.
120 entries: []Entry,
121 allocator: std.mem.Allocator,
122
123 pub fn deinit(self: IconDir) void {
124 self.allocator.free(self.entries);
125 }
126
127 pub const res_header_byte_len = 6;
128
129 pub fn getResDataSize(self: IconDir) u32 {
130 // maxInt(u16) * Entry.res_byte_len = 917,490 which is well within the u32 range.
131 // Note: self.entries.len is limited to maxInt(u16)
132 return @intCast(IconDir.res_header_byte_len + self.entries.len * Entry.res_byte_len);
133 }
134
135 pub fn writeResData(self: IconDir, writer: anytype, first_image_id: u16) !void {
136 try writer.writeIntLittle(u16, 0);
137 try writer.writeIntLittle(u16, @intFromEnum(self.image_type));
138 // We know that entries.len must fit into a u16
139 try writer.writeIntLittle(u16, @as(u16, @intCast(self.entries.len)));
140
141 var image_id = first_image_id;
142 for (self.entries) |entry| {
143 try entry.writeResData(writer, image_id);
144 image_id += 1;
145 }
146 }
147};
148
149pub const Entry = struct {
150 // Icons are limited to u8 sizes, cursors can have u16,
151 // so we store as u16 and truncate when needed.
152 width: u16,
153 height: u16,
154 num_colors: u8,
155 /// This should always be zero, but whatever value it is gets
156 /// carried over so we need to store it
157 reserved: u8,
158 type_specific_data: union(ImageType) {
159 icon: struct {
160 color_planes: u16,
161 bits_per_pixel: u16,
162 },
163 cursor: struct {
164 hotspot_x: u16,
165 hotspot_y: u16,
166 },
167 },
168 data_size_in_bytes: u32,
169 data_offset_from_start_of_file: u32,
170
171 pub const res_byte_len = 14;
172
173 pub fn writeResData(self: Entry, writer: anytype, id: u16) !void {
174 switch (self.type_specific_data) {
175 .icon => |icon_data| {
176 try writer.writeIntLittle(u8, @as(u8, @truncate(self.width)));
177 try writer.writeIntLittle(u8, @as(u8, @truncate(self.height)));
178 try writer.writeIntLittle(u8, self.num_colors);
179 try writer.writeIntLittle(u8, self.reserved);
180 try writer.writeIntLittle(u16, icon_data.color_planes);
181 try writer.writeIntLittle(u16, icon_data.bits_per_pixel);
182 try writer.writeIntLittle(u32, self.data_size_in_bytes);
183 },
184 .cursor => |cursor_data| {
185 try writer.writeIntLittle(u16, self.width);
186 try writer.writeIntLittle(u16, self.height);
187 try writer.writeIntLittle(u16, cursor_data.hotspot_x);
188 try writer.writeIntLittle(u16, cursor_data.hotspot_y);
189 try writer.writeIntLittle(u32, self.data_size_in_bytes + 4);
190 },
191 }
192 try writer.writeIntLittle(u16, id);
193 }
194};
195
196test "icon" {
197 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x00\x00\x00\x16\x00\x00\x00" ++ [_]u8{0} ** 16;
198 var fbs = std.io.fixedBufferStream(data);
199 const icon = try read(std.testing.allocator, fbs.reader(), data.len);
200 defer icon.deinit();
201
202 try std.testing.expectEqual(ImageType.icon, icon.image_type);
203 try std.testing.expectEqual(@as(usize, 1), icon.entries.len);
204}
205
206test "icon too many images" {
207 // Note that with verifying that all data sizes are within the file bounds and >= 16,
208 // it's not possible to hit EOF when looking for more RESDIR structures, since they are
209 // themselves 16 bytes long, so we'll always hit ImpossibleDataSize instead.
210 const data = "\x00\x00\x01\x00\x02\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x00\x00\x00\x16\x00\x00\x00" ++ [_]u8{0} ** 16;
211 var fbs = std.io.fixedBufferStream(data);
212 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
213}
214
215test "icon data size past EOF" {
216 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x01\x00\x00\x16\x00\x00\x00" ++ [_]u8{0} ** 16;
217 var fbs = std.io.fixedBufferStream(data);
218 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
219}
220
221test "icon data offset past EOF" {
222 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x10\x00\x00\x00\x17\x00\x00\x00" ++ [_]u8{0} ** 16;
223 var fbs = std.io.fixedBufferStream(data);
224 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
225}
226
227test "icon data size too small" {
228 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x0F\x00\x00\x00\x16\x00\x00\x00";
229 var fbs = std.io.fixedBufferStream(data);
230 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
231}
232
233pub const ImageFormat = enum {
234 dib,
235 png,
236 riff,
237
238 const riff_header = std.mem.readIntNative(u32, "RIFF");
239 const png_signature = std.mem.readIntNative(u64, "\x89PNG\r\n\x1a\n");
240 const ihdr_code = std.mem.readIntNative(u32, "IHDR");
241 const acon_form_type = std.mem.readIntNative(u32, "ACON");
242
243 pub fn detect(header_bytes: *const [16]u8) ImageFormat {
244 if (std.mem.readIntNative(u32, header_bytes[0..4]) == riff_header) return .riff;
245 if (std.mem.readIntNative(u64, header_bytes[0..8]) == png_signature) return .png;
246 return .dib;
247 }
248
249 pub fn validate(format: ImageFormat, header_bytes: *const [16]u8) bool {
250 return switch (format) {
251 .png => std.mem.readIntNative(u32, header_bytes[12..16]) == ihdr_code,
252 .riff => std.mem.readIntNative(u32, header_bytes[8..12]) == acon_form_type,
253 .dib => true,
254 };
255 }
256};
257
258/// Contains only the fields of BITMAPINFOHEADER (WinGDI.h) that are both:
259/// - relevant to what we need, and
260/// - are shared between all versions of BITMAPINFOHEADER (V4, V5).
261pub const BitmapHeader = extern struct {
262 bcSize: u32,
263 bcWidth: i32,
264 bcHeight: i32,
265 bcPlanes: u16,
266 bcBitCount: u16,
267
268 pub fn version(self: *const BitmapHeader) Version {
269 return Version.get(self.bcSize);
270 }
271
272 /// https://en.wikipedia.org/wiki/BMP_file_format#DIB_header_(bitmap_information_header)
273 pub const Version = enum {
274 unknown,
275 @"win2.0", // Windows 2.0 or later
276 @"nt3.1", // Windows NT, 3.1x or later
277 @"nt4.0", // Windows NT 4.0, 95 or later
278 @"nt5.0", // Windows NT 5.0, 98 or later
279
280 pub fn get(header_size: u32) Version {
281 return switch (header_size) {
282 len(.@"win2.0") => .@"win2.0",
283 len(.@"nt3.1") => .@"nt3.1",
284 len(.@"nt4.0") => .@"nt4.0",
285 len(.@"nt5.0") => .@"nt5.0",
286 else => .unknown,
287 };
288 }
289
290 pub fn len(comptime v: Version) comptime_int {
291 return switch (v) {
292 .@"win2.0" => 12,
293 .@"nt3.1" => 40,
294 .@"nt4.0" => 108,
295 .@"nt5.0" => 124,
296 .unknown => unreachable,
297 };
298 }
299
300 pub fn nameForErrorDisplay(v: Version) []const u8 {
301 return switch (v) {
302 .unknown => "unknown",
303 .@"win2.0" => "Windows 2.0 (BITMAPCOREHEADER)",
304 .@"nt3.1" => "Windows NT, 3.1x (BITMAPINFOHEADER)",
305 .@"nt4.0" => "Windows NT 4.0, 95 (BITMAPV4HEADER)",
306 .@"nt5.0" => "Windows NT 5.0, 98 (BITMAPV5HEADER)",
307 };
308 }
309 };
310};
src/resinator/lang.zig created+877
......@@ -0,0 +1,877 @@
1const std = @import("std");
2
3/// This function is specific to how the Win32 RC command line interprets
4/// language IDs specified as integers.
5/// - Always interpreted as hexadecimal, but explicit 0x prefix is also allowed
6/// - Wraps on overflow of u16
7/// - Stops parsing on any invalid hexadecimal digits
8/// - Errors if a digit is not the first char
9/// - `-` (negative) prefix is allowed
10pub fn parseInt(str: []const u8) error{InvalidLanguageId}!u16 {
11 var result: u16 = 0;
12 const radix: u8 = 16;
13 var buf = str;
14
15 const Prefix = enum { none, minus };
16 var prefix: Prefix = .none;
17 switch (buf[0]) {
18 '-' => {
19 prefix = .minus;
20 buf = buf[1..];
21 },
22 else => {},
23 }
24
25 if (buf.len > 2 and buf[0] == '0' and buf[1] == 'x') {
26 buf = buf[2..];
27 }
28
29 for (buf, 0..) |c, i| {
30 const digit = switch (c) {
31 // On invalid digit for the radix, just stop parsing but don't fail
32 'a'...'f', 'A'...'F', '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
33 else => {
34 // First digit must be valid
35 if (i == 0) {
36 return error.InvalidLanguageId;
37 }
38 break;
39 },
40 };
41
42 if (result != 0) {
43 result *%= radix;
44 }
45 result +%= digit;
46 }
47
48 switch (prefix) {
49 .none => {},
50 .minus => result = 0 -% result,
51 }
52
53 return result;
54}
55
56test parseInt {
57 try std.testing.expectEqual(@as(u16, 0x16), try parseInt("16"));
58 try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1A"));
59 try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1Azzzz"));
60 try std.testing.expectEqual(@as(u16, 0xffff), try parseInt("-1"));
61 try std.testing.expectEqual(@as(u16, 0xffea), try parseInt("-0x16"));
62 try std.testing.expectEqual(@as(u16, 0x0), try parseInt("0o100"));
63 try std.testing.expectEqual(@as(u16, 0x1), try parseInt("10001"));
64 try std.testing.expectError(error.InvalidLanguageId, parseInt("--1"));
65 try std.testing.expectError(error.InvalidLanguageId, parseInt("0xha"));
66 try std.testing.expectError(error.InvalidLanguageId, parseInt("¹"));
67 try std.testing.expectError(error.InvalidLanguageId, parseInt("~1"));
68}
69
70/// This function is specific to how the Win32 RC command line interprets
71/// language tags: invalid tags are rejected, but tags that don't have
72/// a specific assigned ID but are otherwise valid enough will get
73/// converted to an ID of LOCALE_CUSTOM_UNSPECIFIED.
74pub fn tagToInt(tag: []const u8) error{InvalidLanguageTag}!u16 {
75 const maybe_id = try tagToId(tag);
76 if (maybe_id) |id| {
77 return @intFromEnum(id);
78 } else {
79 return LOCALE_CUSTOM_UNSPECIFIED;
80 }
81}
82
83pub fn tagToId(tag: []const u8) error{InvalidLanguageTag}!?LanguageId {
84 const parsed = try parse(tag);
85 // There are currently no language tags with assigned IDs that have
86 // multiple suffixes, so we can skip the lookup.
87 if (parsed.multiple_suffixes) return null;
88 const longest_known_tag = comptime blk: {
89 var len = 0;
90 for (@typeInfo(LanguageId).Enum.fields) |field| {
91 if (field.name.len > len) len = field.name.len;
92 }
93 break :blk len;
94 };
95 // If the tag is longer than the longest tag that has an assigned ID,
96 // then we can skip the lookup.
97 if (tag.len > longest_known_tag) return null;
98 var normalized_buf: [longest_known_tag]u8 = undefined;
99 // To allow e.g. `de-de_phoneb` to get looked up as `de-de`, we need to
100 // omit the suffix, but only if the tag contains a valid alternate sort order.
101 var tag_to_normalize = if (parsed.isSuffixValidSortOrder()) tag[0 .. tag.len - (parsed.suffix.?.len + 1)] else tag;
102 const normalized_tag = normalizeTag(tag_to_normalize, &normalized_buf);
103 return std.meta.stringToEnum(LanguageId, normalized_tag) orelse {
104 // special case for a tag that has been mapped to the same ID
105 // twice.
106 if (std.mem.eql(u8, "ff_latn_ng", normalized_tag)) {
107 return LanguageId.ff_ng;
108 }
109 return null;
110 };
111}
112
113test tagToId {
114 try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("ar-ae")).?);
115 try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("AR_AE")).?);
116 try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-ng")).?);
117 // Special case
118 try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-Latn-NG")).?);
119}
120
121test "exhaustive tagToId" {
122 inline for (@typeInfo(LanguageId).Enum.fields) |field| {
123 const id = tagToId(field.name) catch |err| {
124 std.debug.print("tag: {s}\n", .{field.name});
125 return err;
126 };
127 try std.testing.expectEqual(@field(LanguageId, field.name), id orelse {
128 std.debug.print("tag: {s}, got null\n", .{field.name});
129 return error.TestExpectedEqual;
130 });
131 }
132 var buf: [32]u8 = undefined;
133 inline for (valid_alternate_sorts) |parsed_sort| {
134 var fbs = std.io.fixedBufferStream(&buf);
135 const writer = fbs.writer();
136 writer.writeAll(parsed_sort.language_code) catch unreachable;
137 writer.writeAll("-") catch unreachable;
138 writer.writeAll(parsed_sort.country_code.?) catch unreachable;
139 writer.writeAll("-") catch unreachable;
140 writer.writeAll(parsed_sort.suffix.?) catch unreachable;
141 const expected_field_name = comptime field: {
142 var name_buf: [5]u8 = undefined;
143 std.mem.copy(u8, &name_buf, parsed_sort.language_code);
144 name_buf[2] = '_';
145 std.mem.copy(u8, name_buf[3..], parsed_sort.country_code.?);
146 break :field name_buf;
147 };
148 const expected = @field(LanguageId, &expected_field_name);
149 const id = tagToId(fbs.getWritten()) catch |err| {
150 std.debug.print("tag: {s}\n", .{fbs.getWritten()});
151 return err;
152 };
153 try std.testing.expectEqual(expected, id orelse {
154 std.debug.print("tag: {s}, expected: {}, got null\n", .{ fbs.getWritten(), expected });
155 return error.TestExpectedEqual;
156 });
157 }
158}
159
160fn normalizeTag(tag: []const u8, buf: []u8) []u8 {
161 std.debug.assert(buf.len >= tag.len);
162 for (tag, 0..) |c, i| {
163 if (c == '-')
164 buf[i] = '_'
165 else
166 buf[i] = std.ascii.toLower(c);
167 }
168 return buf[0..tag.len];
169}
170
171/// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-LCID/%5bMS-LCID%5d.pdf#%5B%7B%22num%22%3A72%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C69%2C574%2C0%5D
172/// "When an LCID is requested for a locale without a
173/// permanent LCID assignment, nor a temporary
174/// assignment as above, the protocol will respond
175/// with LOCALE_CUSTOM_UNSPECIFIED for all such
176/// locales. Because this single value is used for
177/// numerous possible locale names, it is impossible to
178/// round trip this locale, even temporarily.
179/// Applications should discard this value as soon as
180/// possible and never persist it. If the system is
181/// forced to respond to a request for
182/// LCID_CUSTOM_UNSPECIFIED, it will fall back to
183/// the current user locale. This is often incorrect but
184/// may prevent an application or component from
185/// failing. As the meaning of this temporary LCID is
186/// unstable, it should never be used for interchange
187/// or persisted data. This is a 1-to-many relationship
188/// that is very unstable."
189pub const LOCALE_CUSTOM_UNSPECIFIED = 0x1000;
190
191pub const LANG_ENGLISH = 0x09;
192pub const SUBLANG_ENGLISH_US = 0x01;
193
194/// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers
195pub fn MAKELANGID(primary: u10, sublang: u6) u16 {
196 return (@as(u16, primary) << 10) | sublang;
197}
198
199/// Language tag format expressed as a regular expression (rough approximation):
200///
201/// [a-zA-Z]{1,3}([-_][a-zA-Z]{4})?([-_][a-zA-Z]{2})?([-_][a-zA-Z0-9]{1,8})?
202/// lang | script | country | suffix
203///
204/// Notes:
205/// - If lang code is 1 char, it seems to mean that everything afterwards uses suffix
206/// parsing rules (e.g. `a-0` and `a-00000000` are allowed).
207/// - There can also be any number of trailing suffix parts as long as they each
208/// would be a valid suffix part, e.g. `en-us-blah-blah1-blah2-blah3` is allowed.
209/// - When doing lookups, trailing suffix parts are taken into account, e.g.
210/// `ca-es-valencia` is not considered equivalent to `ca-es-valencia-blah`.
211/// - A suffix is only allowed if:
212/// + Lang code is 1 char long, or
213/// + A country code is present, or
214/// + A script tag is not present and:
215/// - the suffix is numeric-only and has a length of 3, or
216/// - the lang is `qps` and the suffix is `ploca` or `plocm`
217pub fn parse(lang_tag: []const u8) error{InvalidLanguageTag}!Parsed {
218 var it = std.mem.splitAny(u8, lang_tag, "-_");
219 const lang_code = it.first();
220 const is_valid_lang_code = lang_code.len >= 1 and lang_code.len <= 3 and isAllAlphabetic(lang_code);
221 if (!is_valid_lang_code) return error.InvalidLanguageTag;
222 var parsed = Parsed{
223 .language_code = lang_code,
224 };
225 // The second part could be a script tag, a country code, or a suffix
226 if (it.next()) |part_str| {
227 // The lang code being length 1 behaves strangely, so fully special case it.
228 if (lang_code.len == 1) {
229 // This is almost certainly not the 'right' way to do this, but I don't have a method
230 // to determine how exactly these language tags are parsed, and it seems like
231 // suffix parsing rules apply generally (digits allowed, length of 1 to 8).
232 //
233 // However, because we want to be able to lookup `x-iv-mathan` normally without
234 // `multiple_suffixes` being set to true, we need to make sure to treat two-length
235 // alphabetic parts as a country code.
236 if (part_str.len == 2 and isAllAlphabetic(part_str)) {
237 parsed.country_code = part_str;
238 }
239 // Everything else, though, we can just throw into the suffix as long as the normal
240 // rules apply.
241 else if (part_str.len > 0 and part_str.len <= 8 and isAllAlphanumeric(part_str)) {
242 parsed.suffix = part_str;
243 } else {
244 return error.InvalidLanguageTag;
245 }
246 } else if (part_str.len == 4 and isAllAlphabetic(part_str)) {
247 parsed.script_tag = part_str;
248 } else if (part_str.len == 2 and isAllAlphabetic(part_str)) {
249 parsed.country_code = part_str;
250 }
251 // Only a 3-len numeric suffix is allowed as the second part of a tag
252 else if (part_str.len == 3 and isAllNumeric(part_str)) {
253 parsed.suffix = part_str;
254 }
255 // Special case for qps-ploca and qps-plocm
256 else if (std.ascii.eqlIgnoreCase(lang_code, "qps") and
257 (std.ascii.eqlIgnoreCase(part_str, "ploca") or
258 std.ascii.eqlIgnoreCase(part_str, "plocm")))
259 {
260 parsed.suffix = part_str;
261 } else {
262 return error.InvalidLanguageTag;
263 }
264 } else {
265 // If there's no part besides a 1-len lang code, then it is malformed
266 if (lang_code.len == 1) return error.InvalidLanguageTag;
267 return parsed;
268 }
269 if (parsed.script_tag != null) {
270 if (it.next()) |part_str| {
271 if (part_str.len == 2 and isAllAlphabetic(part_str)) {
272 parsed.country_code = part_str;
273 } else {
274 // Suffix is not allowed when a country code is not present.
275 return error.InvalidLanguageTag;
276 }
277 } else {
278 return parsed;
279 }
280 }
281 // We've now parsed any potential script tag/country codes, so anything remaining
282 // is a suffix
283 while (it.next()) |part_str| {
284 if (part_str.len == 0 or part_str.len > 8 or !isAllAlphanumeric(part_str)) {
285 return error.InvalidLanguageTag;
286 }
287 if (parsed.suffix == null) {
288 parsed.suffix = part_str;
289 } else {
290 // In theory we could return early here but we still want to validate
291 // that each part is a valid suffix all the way to the end, e.g.
292 // we should reject `en-us-suffix-a-b-c-!!!` because of the invalid `!!!`
293 // suffix part.
294 parsed.multiple_suffixes = true;
295 }
296 }
297 return parsed;
298}
299
300pub const Parsed = struct {
301 language_code: []const u8,
302 script_tag: ?[]const u8 = null,
303 country_code: ?[]const u8 = null,
304 /// Can be a sort order (e.g. phoneb) or something like valencia, 001, etc
305 suffix: ?[]const u8 = null,
306 /// There can be any number of suffixes, but we don't need to care what their
307 /// values are, we just need to know if any exist so that e.g. `ca-es-valencia-blah`
308 /// can be seen as different from `ca-es-valencia`. Storing this as a bool
309 /// allows us to avoid needing either (a) dynamic allocation or (b) a limit to
310 /// the number of suffixes allowed when parsing.
311 multiple_suffixes: bool = false,
312
313 pub fn isSuffixValidSortOrder(self: Parsed) bool {
314 if (self.country_code == null) return false;
315 if (self.suffix == null) return false;
316 if (self.script_tag != null) return false;
317 if (self.multiple_suffixes) return false;
318 for (valid_alternate_sorts) |valid_sort| {
319 if (std.ascii.eqlIgnoreCase(valid_sort.language_code, self.language_code) and
320 std.ascii.eqlIgnoreCase(valid_sort.country_code.?, self.country_code.?) and
321 std.ascii.eqlIgnoreCase(valid_sort.suffix.?, self.suffix.?))
322 {
323 return true;
324 }
325 }
326 return false;
327 }
328};
329
330/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
331/// See the table following this text: "Alternate sorts can be selected by using one of the identifiers from the following table."
332const valid_alternate_sorts = [_]Parsed{
333 // Note: x-IV-mathan is omitted due to how lookups are implemented.
334 // This table is used to make e.g. `de-de_phoneb` get looked up
335 // as `de-de` (the suffix is omitted for the lookup), but x-iv-mathan
336 // instead needs to be looked up with the suffix included because
337 // `x-iv` is not a tag with an assigned ID.
338 .{ .language_code = "de", .country_code = "de", .suffix = "phoneb" },
339 .{ .language_code = "hu", .country_code = "hu", .suffix = "tchncl" },
340 .{ .language_code = "ka", .country_code = "ge", .suffix = "modern" },
341 .{ .language_code = "zh", .country_code = "cn", .suffix = "stroke" },
342 .{ .language_code = "zh", .country_code = "sg", .suffix = "stroke" },
343 .{ .language_code = "zh", .country_code = "mo", .suffix = "stroke" },
344 .{ .language_code = "zh", .country_code = "tw", .suffix = "pronun" },
345 .{ .language_code = "zh", .country_code = "tw", .suffix = "radstr" },
346 .{ .language_code = "ja", .country_code = "jp", .suffix = "radstr" },
347 .{ .language_code = "zh", .country_code = "hk", .suffix = "radstr" },
348 .{ .language_code = "zh", .country_code = "mo", .suffix = "radstr" },
349 .{ .language_code = "zh", .country_code = "cn", .suffix = "phoneb" },
350 .{ .language_code = "zh", .country_code = "sg", .suffix = "phoneb" },
351};
352
353test "parse" {
354 try std.testing.expectEqualDeep(Parsed{
355 .language_code = "en",
356 }, try parse("en"));
357 try std.testing.expectEqualDeep(Parsed{
358 .language_code = "en",
359 .country_code = "us",
360 }, try parse("en-us"));
361 try std.testing.expectEqualDeep(Parsed{
362 .language_code = "en",
363 .suffix = "123",
364 }, try parse("en-123"));
365 try std.testing.expectEqualDeep(Parsed{
366 .language_code = "en",
367 .suffix = "123",
368 .multiple_suffixes = true,
369 }, try parse("en-123-blah"));
370 try std.testing.expectEqualDeep(Parsed{
371 .language_code = "en",
372 .country_code = "us",
373 .suffix = "123",
374 .multiple_suffixes = true,
375 }, try parse("en-us_123-blah"));
376 try std.testing.expectEqualDeep(Parsed{
377 .language_code = "eng",
378 .script_tag = "Latn",
379 }, try parse("eng-Latn"));
380 try std.testing.expectEqualDeep(Parsed{
381 .language_code = "eng",
382 .script_tag = "Latn",
383 }, try parse("eng-Latn"));
384 try std.testing.expectEqualDeep(Parsed{
385 .language_code = "ff",
386 .script_tag = "Latn",
387 .country_code = "NG",
388 }, try parse("ff-Latn-NG"));
389 try std.testing.expectEqualDeep(Parsed{
390 .language_code = "qps",
391 .suffix = "Plocm",
392 }, try parse("qps-Plocm"));
393 try std.testing.expectEqualDeep(Parsed{
394 .language_code = "qps",
395 .suffix = "ploca",
396 }, try parse("qps-ploca"));
397 try std.testing.expectEqualDeep(Parsed{
398 .language_code = "x",
399 .country_code = "IV",
400 .suffix = "mathan",
401 }, try parse("x-IV-mathan"));
402 try std.testing.expectEqualDeep(Parsed{
403 .language_code = "a",
404 .suffix = "a",
405 }, try parse("a-a"));
406 try std.testing.expectEqualDeep(Parsed{
407 .language_code = "a",
408 .suffix = "000",
409 }, try parse("a-000"));
410 try std.testing.expectEqualDeep(Parsed{
411 .language_code = "a",
412 .suffix = "00000000",
413 }, try parse("a-00000000"));
414 // suffix not allowed if script tag is present without country code
415 try std.testing.expectError(error.InvalidLanguageTag, parse("eng-Latn-suffix"));
416 // suffix must be 3 numeric digits if neither script tag nor country code is present
417 try std.testing.expectError(error.InvalidLanguageTag, parse("eng-suffix"));
418 try std.testing.expectError(error.InvalidLanguageTag, parse("en-plocm"));
419 // 1-len lang code is not allowed if it's the only part
420 try std.testing.expectError(error.InvalidLanguageTag, parse("e"));
421}
422
423fn isAllAlphabetic(str: []const u8) bool {
424 for (str) |c| {
425 if (!std.ascii.isAlphabetic(c)) return false;
426 }
427 return true;
428}
429
430fn isAllAlphanumeric(str: []const u8) bool {
431 for (str) |c| {
432 if (!std.ascii.isAlphanumeric(c)) return false;
433 }
434 return true;
435}
436
437fn isAllNumeric(str: []const u8) bool {
438 for (str) |c| {
439 if (!std.ascii.isDigit(c)) return false;
440 }
441 return true;
442}
443
444/// Derived from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
445/// - Protocol Revision: 15.0
446/// - Language / Language ID / Language Tag table in Appendix A
447/// - Removed all rows that have Language ID 0x1000 (LOCALE_CUSTOM_UNSPECIFIED)
448/// - Normalized each language tag (lowercased, replaced all `-` with `_`)
449/// - There is one special case where two tags are mapped to the same ID, the following
450/// has been omitted and must be special cased during lookup to map to the ID ff_ng / 0x0467.
451/// ff_latn_ng = 0x0467, // Fulah (Latin), Nigeria
452/// - x_iv_mathan has been added which is not in the table but does appear in the Alternate sorts
453/// table as 0x007F (LANG_INVARIANT).
454pub const LanguageId = enum(u16) {
455 // Language tag = Language ID, // Language, Location (or type)
456 af = 0x0036, // Afrikaans
457 af_za = 0x0436, // Afrikaans, South Africa
458 sq = 0x001C, // Albanian
459 sq_al = 0x041C, // Albanian, Albania
460 gsw = 0x0084, // Alsatian
461 gsw_fr = 0x0484, // Alsatian, France
462 am = 0x005E, // Amharic
463 am_et = 0x045E, // Amharic, Ethiopia
464 ar = 0x0001, // Arabic
465 ar_dz = 0x1401, // Arabic, Algeria
466 ar_bh = 0x3C01, // Arabic, Bahrain
467 ar_eg = 0x0c01, // Arabic, Egypt
468 ar_iq = 0x0801, // Arabic, Iraq
469 ar_jo = 0x2C01, // Arabic, Jordan
470 ar_kw = 0x3401, // Arabic, Kuwait
471 ar_lb = 0x3001, // Arabic, Lebanon
472 ar_ly = 0x1001, // Arabic, Libya
473 ar_ma = 0x1801, // Arabic, Morocco
474 ar_om = 0x2001, // Arabic, Oman
475 ar_qa = 0x4001, // Arabic, Qatar
476 ar_sa = 0x0401, // Arabic, Saudi Arabia
477 ar_sy = 0x2801, // Arabic, Syria
478 ar_tn = 0x1C01, // Arabic, Tunisia
479 ar_ae = 0x3801, // Arabic, U.A.E.
480 ar_ye = 0x2401, // Arabic, Yemen
481 hy = 0x002B, // Armenian
482 hy_am = 0x042B, // Armenian, Armenia
483 as = 0x004D, // Assamese
484 as_in = 0x044D, // Assamese, India
485 az_cyrl = 0x742C, // Azerbaijani (Cyrillic)
486 az_cyrl_az = 0x082C, // Azerbaijani (Cyrillic), Azerbaijan
487 az = 0x002C, // Azerbaijani (Latin)
488 az_latn = 0x782C, // Azerbaijani (Latin)
489 az_latn_az = 0x042C, // Azerbaijani (Latin), Azerbaijan
490 bn = 0x0045, // Bangla
491 bn_bd = 0x0845, // Bangla, Bangladesh
492 bn_in = 0x0445, // Bangla, India
493 ba = 0x006D, // Bashkir
494 ba_ru = 0x046D, // Bashkir, Russia
495 eu = 0x002D, // Basque
496 eu_es = 0x042D, // Basque, Spain
497 be = 0x0023, // Belarusian
498 be_by = 0x0423, // Belarusian, Belarus
499 bs_cyrl = 0x641A, // Bosnian (Cyrillic)
500 bs_cyrl_ba = 0x201A, // Bosnian (Cyrillic), Bosnia and Herzegovina
501 bs_latn = 0x681A, // Bosnian (Latin)
502 bs = 0x781A, // Bosnian (Latin)
503 bs_latn_ba = 0x141A, // Bosnian (Latin), Bosnia and Herzegovina
504 br = 0x007E, // Breton
505 br_fr = 0x047E, // Breton, France
506 bg = 0x0002, // Bulgarian
507 bg_bg = 0x0402, // Bulgarian, Bulgaria
508 my = 0x0055, // Burmese
509 my_mm = 0x0455, // Burmese, Myanmar
510 ca = 0x0003, // Catalan
511 ca_es = 0x0403, // Catalan, Spain
512 tzm_arab_ma = 0x045F, // Central Atlas Tamazight (Arabic), Morocco
513 ku = 0x0092, // Central Kurdish
514 ku_arab = 0x7c92, // Central Kurdish
515 ku_arab_iq = 0x0492, // Central Kurdish, Iraq
516 chr = 0x005C, // Cherokee
517 chr_cher = 0x7c5C, // Cherokee
518 chr_cher_us = 0x045C, // Cherokee, United States
519 zh_hans = 0x0004, // Chinese (Simplified)
520 zh = 0x7804, // Chinese (Simplified)
521 zh_cn = 0x0804, // Chinese (Simplified), People's Republic of China
522 zh_sg = 0x1004, // Chinese (Simplified), Singapore
523 zh_hant = 0x7C04, // Chinese (Traditional)
524 zh_hk = 0x0C04, // Chinese (Traditional), Hong Kong S.A.R.
525 zh_mo = 0x1404, // Chinese (Traditional), Macao S.A.R.
526 zh_tw = 0x0404, // Chinese (Traditional), Taiwan
527 co = 0x0083, // Corsican
528 co_fr = 0x0483, // Corsican, France
529 hr = 0x001A, // Croatian
530 hr_hr = 0x041A, // Croatian, Croatia
531 hr_ba = 0x101A, // Croatian (Latin), Bosnia and Herzegovina
532 cs = 0x0005, // Czech
533 cs_cz = 0x0405, // Czech, Czech Republic
534 da = 0x0006, // Danish
535 da_dk = 0x0406, // Danish, Denmark
536 prs = 0x008C, // Dari
537 prs_af = 0x048C, // Dari, Afghanistan
538 dv = 0x0065, // Divehi
539 dv_mv = 0x0465, // Divehi, Maldives
540 nl = 0x0013, // Dutch
541 nl_be = 0x0813, // Dutch, Belgium
542 nl_nl = 0x0413, // Dutch, Netherlands
543 dz_bt = 0x0C51, // Dzongkha, Bhutan
544 en = 0x0009, // English
545 en_au = 0x0C09, // English, Australia
546 en_bz = 0x2809, // English, Belize
547 en_ca = 0x1009, // English, Canada
548 en_029 = 0x2409, // English, Caribbean
549 en_hk = 0x3C09, // English, Hong Kong
550 en_in = 0x4009, // English, India
551 en_ie = 0x1809, // English, Ireland
552 en_jm = 0x2009, // English, Jamaica
553 en_my = 0x4409, // English, Malaysia
554 en_nz = 0x1409, // English, New Zealand
555 en_ph = 0x3409, // English, Republic of the Philippines
556 en_sg = 0x4809, // English, Singapore
557 en_za = 0x1C09, // English, South Africa
558 en_tt = 0x2c09, // English, Trinidad and Tobago
559 en_ae = 0x4C09, // English, United Arab Emirates
560 en_gb = 0x0809, // English, United Kingdom
561 en_us = 0x0409, // English, United States
562 en_zw = 0x3009, // English, Zimbabwe
563 et = 0x0025, // Estonian
564 et_ee = 0x0425, // Estonian, Estonia
565 fo = 0x0038, // Faroese
566 fo_fo = 0x0438, // Faroese, Faroe Islands
567 fil = 0x0064, // Filipino
568 fil_ph = 0x0464, // Filipino, Philippines
569 fi = 0x000B, // Finnish
570 fi_fi = 0x040B, // Finnish, Finland
571 fr = 0x000C, // French
572 fr_be = 0x080C, // French, Belgium
573 fr_cm = 0x2c0C, // French, Cameroon
574 fr_ca = 0x0c0C, // French, Canada
575 fr_029 = 0x1C0C, // French, Caribbean
576 fr_cd = 0x240C, // French, Congo, DRC
577 fr_ci = 0x300C, // French, Côte d'Ivoire
578 fr_fr = 0x040C, // French, France
579 fr_ht = 0x3c0C, // French, Haiti
580 fr_lu = 0x140C, // French, Luxembourg
581 fr_ml = 0x340C, // French, Mali
582 fr_ma = 0x380C, // French, Morocco
583 fr_mc = 0x180C, // French, Principality of Monaco
584 fr_re = 0x200C, // French, Reunion
585 fr_sn = 0x280C, // French, Senegal
586 fr_ch = 0x100C, // French, Switzerland
587 fy = 0x0062, // Frisian
588 fy_nl = 0x0462, // Frisian, Netherlands
589 ff = 0x0067, // Fulah
590 ff_latn = 0x7C67, // Fulah (Latin)
591 ff_ng = 0x0467, // Fulah, Nigeria
592 ff_latn_sn = 0x0867, // Fulah, Senegal
593 gl = 0x0056, // Galician
594 gl_es = 0x0456, // Galician, Spain
595 ka = 0x0037, // Georgian
596 ka_ge = 0x0437, // Georgian, Georgia
597 de = 0x0007, // German
598 de_at = 0x0C07, // German, Austria
599 de_de = 0x0407, // German, Germany
600 de_li = 0x1407, // German, Liechtenstein
601 de_lu = 0x1007, // German, Luxembourg
602 de_ch = 0x0807, // German, Switzerland
603 el = 0x0008, // Greek
604 el_gr = 0x0408, // Greek, Greece
605 kl = 0x006F, // Greenlandic
606 kl_gl = 0x046F, // Greenlandic, Greenland
607 gn = 0x0074, // Guarani
608 gn_py = 0x0474, // Guarani, Paraguay
609 gu = 0x0047, // Gujarati
610 gu_in = 0x0447, // Gujarati, India
611 ha = 0x0068, // Hausa (Latin)
612 ha_latn = 0x7C68, // Hausa (Latin)
613 ha_latn_ng = 0x0468, // Hausa (Latin), Nigeria
614 haw = 0x0075, // Hawaiian
615 haw_us = 0x0475, // Hawaiian, United States
616 he = 0x000D, // Hebrew
617 he_il = 0x040D, // Hebrew, Israel
618 hi = 0x0039, // Hindi
619 hi_in = 0x0439, // Hindi, India
620 hu = 0x000E, // Hungarian
621 hu_hu = 0x040E, // Hungarian, Hungary
622 is = 0x000F, // Icelandic
623 is_is = 0x040F, // Icelandic, Iceland
624 ig = 0x0070, // Igbo
625 ig_ng = 0x0470, // Igbo, Nigeria
626 id = 0x0021, // Indonesian
627 id_id = 0x0421, // Indonesian, Indonesia
628 iu = 0x005D, // Inuktitut (Latin)
629 iu_latn = 0x7C5D, // Inuktitut (Latin)
630 iu_latn_ca = 0x085D, // Inuktitut (Latin), Canada
631 iu_cans = 0x785D, // Inuktitut (Syllabics)
632 iu_cans_ca = 0x045d, // Inuktitut (Syllabics), Canada
633 ga = 0x003C, // Irish
634 ga_ie = 0x083C, // Irish, Ireland
635 it = 0x0010, // Italian
636 it_it = 0x0410, // Italian, Italy
637 it_ch = 0x0810, // Italian, Switzerland
638 ja = 0x0011, // Japanese
639 ja_jp = 0x0411, // Japanese, Japan
640 kn = 0x004B, // Kannada
641 kn_in = 0x044B, // Kannada, India
642 kr_latn_ng = 0x0471, // Kanuri (Latin), Nigeria
643 ks = 0x0060, // Kashmiri
644 ks_arab = 0x0460, // Kashmiri, Perso-Arabic
645 ks_deva_in = 0x0860, // Kashmiri (Devanagari), India
646 kk = 0x003F, // Kazakh
647 kk_kz = 0x043F, // Kazakh, Kazakhstan
648 km = 0x0053, // Khmer
649 km_kh = 0x0453, // Khmer, Cambodia
650 quc = 0x0086, // K'iche
651 quc_latn_gt = 0x0486, // K'iche, Guatemala
652 rw = 0x0087, // Kinyarwanda
653 rw_rw = 0x0487, // Kinyarwanda, Rwanda
654 sw = 0x0041, // Kiswahili
655 sw_ke = 0x0441, // Kiswahili, Kenya
656 kok = 0x0057, // Konkani
657 kok_in = 0x0457, // Konkani, India
658 ko = 0x0012, // Korean
659 ko_kr = 0x0412, // Korean, Korea
660 ky = 0x0040, // Kyrgyz
661 ky_kg = 0x0440, // Kyrgyz, Kyrgyzstan
662 lo = 0x0054, // Lao
663 lo_la = 0x0454, // Lao, Lao P.D.R.
664 la_va = 0x0476, // Latin, Vatican City
665 lv = 0x0026, // Latvian
666 lv_lv = 0x0426, // Latvian, Latvia
667 lt = 0x0027, // Lithuanian
668 lt_lt = 0x0427, // Lithuanian, Lithuania
669 dsb = 0x7C2E, // Lower Sorbian
670 dsb_de = 0x082E, // Lower Sorbian, Germany
671 lb = 0x006E, // Luxembourgish
672 lb_lu = 0x046E, // Luxembourgish, Luxembourg
673 mk = 0x002F, // Macedonian
674 mk_mk = 0x042F, // Macedonian, North Macedonia
675 ms = 0x003E, // Malay
676 ms_bn = 0x083E, // Malay, Brunei Darussalam
677 ms_my = 0x043E, // Malay, Malaysia
678 ml = 0x004C, // Malayalam
679 ml_in = 0x044C, // Malayalam, India
680 mt = 0x003A, // Maltese
681 mt_mt = 0x043A, // Maltese, Malta
682 mi = 0x0081, // Maori
683 mi_nz = 0x0481, // Maori, New Zealand
684 arn = 0x007A, // Mapudungun
685 arn_cl = 0x047A, // Mapudungun, Chile
686 mr = 0x004E, // Marathi
687 mr_in = 0x044E, // Marathi, India
688 moh = 0x007C, // Mohawk
689 moh_ca = 0x047C, // Mohawk, Canada
690 mn = 0x0050, // Mongolian (Cyrillic)
691 mn_cyrl = 0x7850, // Mongolian (Cyrillic)
692 mn_mn = 0x0450, // Mongolian (Cyrillic), Mongolia
693 mn_mong = 0x7C50, // Mongolian (Traditional Mongolian)
694 mn_mong_cn = 0x0850, // Mongolian (Traditional Mongolian), People's Republic of China
695 mn_mong_mn = 0x0C50, // Mongolian (Traditional Mongolian), Mongolia
696 ne = 0x0061, // Nepali
697 ne_in = 0x0861, // Nepali, India
698 ne_np = 0x0461, // Nepali, Nepal
699 no = 0x0014, // Norwegian (Bokmal)
700 nb = 0x7C14, // Norwegian (Bokmal)
701 nb_no = 0x0414, // Norwegian (Bokmal), Norway
702 nn = 0x7814, // Norwegian (Nynorsk)
703 nn_no = 0x0814, // Norwegian (Nynorsk), Norway
704 oc = 0x0082, // Occitan
705 oc_fr = 0x0482, // Occitan, France
706 @"or" = 0x0048, // Odia
707 or_in = 0x0448, // Odia, India
708 om = 0x0072, // Oromo
709 om_et = 0x0472, // Oromo, Ethiopia
710 ps = 0x0063, // Pashto
711 ps_af = 0x0463, // Pashto, Afghanistan
712 fa = 0x0029, // Persian
713 fa_ir = 0x0429, // Persian, Iran
714 pl = 0x0015, // Polish
715 pl_pl = 0x0415, // Polish, Poland
716 pt = 0x0016, // Portuguese
717 pt_br = 0x0416, // Portuguese, Brazil
718 pt_pt = 0x0816, // Portuguese, Portugal
719 qps_ploca = 0x05FE, // Pseudo Language, Pseudo locale for east Asian/complex script localization testing
720 qps_ploc = 0x0501, // Pseudo Language, Pseudo locale used for localization testing
721 qps_plocm = 0x09FF, // Pseudo Language, Pseudo locale used for localization testing of mirrored locales
722 pa = 0x0046, // Punjabi
723 pa_arab = 0x7C46, // Punjabi
724 pa_in = 0x0446, // Punjabi, India
725 pa_arab_pk = 0x0846, // Punjabi, Islamic Republic of Pakistan
726 quz = 0x006B, // Quechua
727 quz_bo = 0x046B, // Quechua, Bolivia
728 quz_ec = 0x086B, // Quechua, Ecuador
729 quz_pe = 0x0C6B, // Quechua, Peru
730 ro = 0x0018, // Romanian
731 ro_md = 0x0818, // Romanian, Moldova
732 ro_ro = 0x0418, // Romanian, Romania
733 rm = 0x0017, // Romansh
734 rm_ch = 0x0417, // Romansh, Switzerland
735 ru = 0x0019, // Russian
736 ru_md = 0x0819, // Russian, Moldova
737 ru_ru = 0x0419, // Russian, Russia
738 sah = 0x0085, // Sakha
739 sah_ru = 0x0485, // Sakha, Russia
740 smn = 0x703B, // Sami (Inari)
741 smn_fi = 0x243B, // Sami (Inari), Finland
742 smj = 0x7C3B, // Sami (Lule)
743 smj_no = 0x103B, // Sami (Lule), Norway
744 smj_se = 0x143B, // Sami (Lule), Sweden
745 se = 0x003B, // Sami (Northern)
746 se_fi = 0x0C3B, // Sami (Northern), Finland
747 se_no = 0x043B, // Sami (Northern), Norway
748 se_se = 0x083B, // Sami (Northern), Sweden
749 sms = 0x743B, // Sami (Skolt)
750 sms_fi = 0x203B, // Sami (Skolt), Finland
751 sma = 0x783B, // Sami (Southern)
752 sma_no = 0x183B, // Sami (Southern), Norway
753 sma_se = 0x1C3B, // Sami (Southern), Sweden
754 sa = 0x004F, // Sanskrit
755 sa_in = 0x044F, // Sanskrit, India
756 gd = 0x0091, // Scottish Gaelic
757 gd_gb = 0x0491, // Scottish Gaelic, United Kingdom
758 sr_cyrl = 0x6C1A, // Serbian (Cyrillic)
759 sr_cyrl_ba = 0x1C1A, // Serbian (Cyrillic), Bosnia and Herzegovina
760 sr_cyrl_me = 0x301A, // Serbian (Cyrillic), Montenegro
761 sr_cyrl_rs = 0x281A, // Serbian (Cyrillic), Serbia
762 sr_cyrl_cs = 0x0C1A, // Serbian (Cyrillic), Serbia and Montenegro (Former)
763 sr_latn = 0x701A, // Serbian (Latin)
764 sr = 0x7C1A, // Serbian (Latin)
765 sr_latn_ba = 0x181A, // Serbian (Latin), Bosnia and Herzegovina
766 sr_latn_me = 0x2c1A, // Serbian (Latin), Montenegro
767 sr_latn_rs = 0x241A, // Serbian (Latin), Serbia
768 sr_latn_cs = 0x081A, // Serbian (Latin), Serbia and Montenegro (Former)
769 nso = 0x006C, // Sesotho sa Leboa
770 nso_za = 0x046C, // Sesotho sa Leboa, South Africa
771 tn = 0x0032, // Setswana
772 tn_bw = 0x0832, // Setswana, Botswana
773 tn_za = 0x0432, // Setswana, South Africa
774 sd = 0x0059, // Sindhi
775 sd_arab = 0x7C59, // Sindhi
776 sd_arab_pk = 0x0859, // Sindhi, Islamic Republic of Pakistan
777 si = 0x005B, // Sinhala
778 si_lk = 0x045B, // Sinhala, Sri Lanka
779 sk = 0x001B, // Slovak
780 sk_sk = 0x041B, // Slovak, Slovakia
781 sl = 0x0024, // Slovenian
782 sl_si = 0x0424, // Slovenian, Slovenia
783 so = 0x0077, // Somali
784 so_so = 0x0477, // Somali, Somalia
785 st = 0x0030, // Sotho
786 st_za = 0x0430, // Sotho, South Africa
787 es = 0x000A, // Spanish
788 es_ar = 0x2C0A, // Spanish, Argentina
789 es_ve = 0x200A, // Spanish, Bolivarian Republic of Venezuela
790 es_bo = 0x400A, // Spanish, Bolivia
791 es_cl = 0x340A, // Spanish, Chile
792 es_co = 0x240A, // Spanish, Colombia
793 es_cr = 0x140A, // Spanish, Costa Rica
794 es_cu = 0x5c0A, // Spanish, Cuba
795 es_do = 0x1c0A, // Spanish, Dominican Republic
796 es_ec = 0x300A, // Spanish, Ecuador
797 es_sv = 0x440A, // Spanish, El Salvador
798 es_gt = 0x100A, // Spanish, Guatemala
799 es_hn = 0x480A, // Spanish, Honduras
800 es_419 = 0x580A, // Spanish, Latin America
801 es_mx = 0x080A, // Spanish, Mexico
802 es_ni = 0x4C0A, // Spanish, Nicaragua
803 es_pa = 0x180A, // Spanish, Panama
804 es_py = 0x3C0A, // Spanish, Paraguay
805 es_pe = 0x280A, // Spanish, Peru
806 es_pr = 0x500A, // Spanish, Puerto Rico
807 es_es_tradnl = 0x040A, // Spanish, Spain
808 es_es = 0x0c0A, // Spanish, Spain
809 es_us = 0x540A, // Spanish, United States
810 es_uy = 0x380A, // Spanish, Uruguay
811 sv = 0x001D, // Swedish
812 sv_fi = 0x081D, // Swedish, Finland
813 sv_se = 0x041D, // Swedish, Sweden
814 syr = 0x005A, // Syriac
815 syr_sy = 0x045A, // Syriac, Syria
816 tg = 0x0028, // Tajik (Cyrillic)
817 tg_cyrl = 0x7C28, // Tajik (Cyrillic)
818 tg_cyrl_tj = 0x0428, // Tajik (Cyrillic), Tajikistan
819 tzm = 0x005F, // Tamazight (Latin)
820 tzm_latn = 0x7C5F, // Tamazight (Latin)
821 tzm_latn_dz = 0x085F, // Tamazight (Latin), Algeria
822 ta = 0x0049, // Tamil
823 ta_in = 0x0449, // Tamil, India
824 ta_lk = 0x0849, // Tamil, Sri Lanka
825 tt = 0x0044, // Tatar
826 tt_ru = 0x0444, // Tatar, Russia
827 te = 0x004A, // Telugu
828 te_in = 0x044A, // Telugu, India
829 th = 0x001E, // Thai
830 th_th = 0x041E, // Thai, Thailand
831 bo = 0x0051, // Tibetan
832 bo_cn = 0x0451, // Tibetan, People's Republic of China
833 ti = 0x0073, // Tigrinya
834 ti_er = 0x0873, // Tigrinya, Eritrea
835 ti_et = 0x0473, // Tigrinya, Ethiopia
836 ts = 0x0031, // Tsonga
837 ts_za = 0x0431, // Tsonga, South Africa
838 tr = 0x001F, // Turkish
839 tr_tr = 0x041F, // Turkish, Turkey
840 tk = 0x0042, // Turkmen
841 tk_tm = 0x0442, // Turkmen, Turkmenistan
842 uk = 0x0022, // Ukrainian
843 uk_ua = 0x0422, // Ukrainian, Ukraine
844 hsb = 0x002E, // Upper Sorbian
845 hsb_de = 0x042E, // Upper Sorbian, Germany
846 ur = 0x0020, // Urdu
847 ur_in = 0x0820, // Urdu, India
848 ur_pk = 0x0420, // Urdu, Islamic Republic of Pakistan
849 ug = 0x0080, // Uyghur
850 ug_cn = 0x0480, // Uyghur, People's Republic of China
851 uz_cyrl = 0x7843, // Uzbek (Cyrillic)
852 uz_cyrl_uz = 0x0843, // Uzbek (Cyrillic), Uzbekistan
853 uz = 0x0043, // Uzbek (Latin)
854 uz_latn = 0x7C43, // Uzbek (Latin)
855 uz_latn_uz = 0x0443, // Uzbek (Latin), Uzbekistan
856 ca_es_valencia = 0x0803, // Valencian, Spain
857 ve = 0x0033, // Venda
858 ve_za = 0x0433, // Venda, South Africa
859 vi = 0x002A, // Vietnamese
860 vi_vn = 0x042A, // Vietnamese, Vietnam
861 cy = 0x0052, // Welsh
862 cy_gb = 0x0452, // Welsh, United Kingdom
863 wo = 0x0088, // Wolof
864 wo_sn = 0x0488, // Wolof, Senegal
865 xh = 0x0034, // Xhosa
866 xh_za = 0x0434, // Xhosa, South Africa
867 ii = 0x0078, // Yi
868 ii_cn = 0x0478, // Yi, People's Republic of China
869 yi_001 = 0x043D, // Yiddish, World
870 yo = 0x006A, // Yoruba
871 yo_ng = 0x046A, // Yoruba, Nigeria
872 zu = 0x0035, // Zulu
873 zu_za = 0x0435, // Zulu, South Africa
874
875 /// Special case
876 x_iv_mathan = 0x007F, // LANG_INVARIANT, "math alphanumeric sorting"
877};
src/resinator/lex.zig created+1104
......@@ -0,0 +1,1104 @@
1//! Expects to be run after the C preprocessor and after `removeComments`.
2//! This means that the lexer assumes that:
3//! - Splices ('\' at the end of a line) have been handled/collapsed.
4//! - Preprocessor directives and macros have been expanded (any remaining should be skipped with the exception of `#pragma code_page`).
5//! - All comments have been removed.
6
7const std = @import("std");
8const ErrorDetails = @import("errors.zig").ErrorDetails;
9const columnsUntilTabStop = @import("literals.zig").columnsUntilTabStop;
10const code_pages = @import("code_pages.zig");
11const CodePage = code_pages.CodePage;
12const SourceMappings = @import("source_mapping.zig").SourceMappings;
13const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
14
15const dumpTokensDuringTests = false;
16
17pub const default_max_string_literal_codepoints = 4097;
18
19pub const Token = struct {
20 id: Id,
21 start: usize,
22 end: usize,
23 line_number: usize,
24
25 pub const Id = enum {
26 literal,
27 number,
28 quoted_ascii_string,
29 quoted_wide_string,
30 operator,
31 begin,
32 end,
33 comma,
34 open_paren,
35 close_paren,
36 /// This Id is only used for errors, the Lexer will never return one
37 /// of these from a `next` call.
38 preprocessor_command,
39 invalid,
40 eof,
41
42 pub fn nameForErrorDisplay(self: Id) []const u8 {
43 return switch (self) {
44 .literal => "<literal>",
45 .number => "<number>",
46 .quoted_ascii_string => "<quoted ascii string>",
47 .quoted_wide_string => "<quoted wide string>",
48 .operator => "<operator>",
49 .begin => "<'{' or BEGIN>",
50 .end => "<'}' or END>",
51 .comma => ",",
52 .open_paren => "(",
53 .close_paren => ")",
54 .preprocessor_command => "<preprocessor command>",
55 .invalid => unreachable,
56 .eof => "<eof>",
57 };
58 }
59 };
60
61 pub fn slice(self: Token, buffer: []const u8) []const u8 {
62 return buffer[self.start..self.end];
63 }
64
65 pub fn nameForErrorDisplay(self: Token, buffer: []const u8) []const u8 {
66 return switch (self.id) {
67 .eof => self.id.nameForErrorDisplay(),
68 else => self.slice(buffer),
69 };
70 }
71
72 pub fn calculateColumn(token: Token, source: []const u8, tab_columns: usize, maybe_line_start: ?usize) usize {
73 const line_start = maybe_line_start orelse token.getLineStart(source);
74
75 var i: usize = line_start;
76 var column: usize = 0;
77 while (i < token.start) : (i += 1) {
78 const c = source[i];
79 switch (c) {
80 '\t' => column += columnsUntilTabStop(column, tab_columns),
81 else => column += 1,
82 }
83 }
84 return column;
85 }
86
87 // TODO: This doesn't necessarily match up with how we count line numbers, but where a line starts
88 // has a knock-on effect on calculateColumn. More testing is needed to determine what needs
89 // to be changed to make this both (1) match how line numbers are counted and (2) match how
90 // the Win32 RC compiler counts tab columns.
91 //
92 // (the TODO in currentIndexFormsLineEndingPair should be taken into account as well)
93 pub fn getLineStart(token: Token, source: []const u8) usize {
94 const line_start = line_start: {
95 if (token.start != 0) {
96 // start checking at the byte before the token
97 var index = token.start - 1;
98 while (true) {
99 if (source[index] == '\n') break :line_start @min(source.len - 1, index + 1);
100 if (index != 0) index -= 1 else break;
101 }
102 }
103 break :line_start 0;
104 };
105 return line_start;
106 }
107
108 pub fn getLine(token: Token, source: []const u8, maybe_line_start: ?usize) []const u8 {
109 const line_start = maybe_line_start orelse token.getLineStart(source);
110
111 var line_end = line_start + 1;
112 while (line_end < source.len and source[line_end] != '\n') : (line_end += 1) {}
113 while (line_end > 0 and source[line_end - 1] == '\r') : (line_end -= 1) {}
114
115 return source[line_start..line_end];
116 }
117
118 pub fn isStringLiteral(token: Token) bool {
119 return token.id == .quoted_ascii_string or token.id == .quoted_wide_string;
120 }
121};
122
123pub const LineHandler = struct {
124 line_number: usize = 1,
125 buffer: []const u8,
126 last_line_ending_index: ?usize = null,
127
128 /// Like incrementLineNumber but checks that the current char is a line ending first.
129 /// Returns the new line number if it was incremented, null otherwise.
130 pub fn maybeIncrementLineNumber(self: *LineHandler, cur_index: usize) ?usize {
131 const c = self.buffer[cur_index];
132 if (c == '\r' or c == '\n') {
133 return self.incrementLineNumber(cur_index);
134 }
135 return null;
136 }
137
138 /// Increments line_number appropriately (handling line ending pairs)
139 /// and returns the new line number if it was incremented, or null otherwise.
140 pub fn incrementLineNumber(self: *LineHandler, cur_index: usize) ?usize {
141 if (self.currentIndexFormsLineEndingPair(cur_index)) {
142 self.last_line_ending_index = null;
143 return null;
144 } else {
145 self.line_number += 1;
146 self.last_line_ending_index = cur_index;
147 return self.line_number;
148 }
149 }
150
151 /// \r\n and \n\r pairs are treated as a single line ending (but not \r\r \n\n)
152 /// expects self.index and last_line_ending_index (if non-null) to contain line endings
153 ///
154 /// TODO: This is not really how the Win32 RC compiler handles line endings. Instead, it
155 /// seems to drop all carriage returns during preprocessing and then replace all
156 /// remaining line endings with well-formed CRLF pairs (e.g. `<CR>a<CR>b<LF>c` becomes `ab<CR><LF>c`).
157 /// Handling this the same as the Win32 RC compiler would need control over the preprocessor,
158 /// since Clang converts unpaired <CR> into unpaired <LF>.
159 pub fn currentIndexFormsLineEndingPair(self: *const LineHandler, cur_index: usize) bool {
160 if (self.last_line_ending_index == null) return false;
161
162 // must immediately precede the current index, we know cur_index must
163 // be >= 1 since last_line_ending_index is non-null (so if the subtraction
164 // overflows it is a bug at the callsite of this function).
165 if (self.last_line_ending_index.? != cur_index - 1) return false;
166
167 const cur_line_ending = self.buffer[cur_index];
168 const last_line_ending = self.buffer[self.last_line_ending_index.?];
169
170 // sanity check
171 std.debug.assert(cur_line_ending == '\r' or cur_line_ending == '\n');
172 std.debug.assert(last_line_ending == '\r' or last_line_ending == '\n');
173
174 // can't be \n\n or \r\r
175 if (last_line_ending == cur_line_ending) return false;
176
177 return true;
178 }
179};
180
181pub const LexError = error{
182 UnfinishedStringLiteral,
183 StringLiteralTooLong,
184 InvalidNumberWithExponent,
185 InvalidDigitCharacterInNumberLiteral,
186 IllegalByte,
187 IllegalByteOutsideStringLiterals,
188 IllegalCodepointOutsideStringLiterals,
189 IllegalByteOrderMark,
190 IllegalPrivateUseCharacter,
191 FoundCStyleEscapedQuote,
192 CodePagePragmaMissingLeftParen,
193 CodePagePragmaMissingRightParen,
194 /// Can be caught and ignored
195 CodePagePragmaInvalidCodePage,
196 CodePagePragmaNotInteger,
197 CodePagePragmaOverflow,
198 CodePagePragmaUnsupportedCodePage,
199 /// Can be caught and ignored
200 CodePagePragmaInIncludedFile,
201};
202
203pub const Lexer = struct {
204 const Self = @This();
205
206 buffer: []const u8,
207 index: usize,
208 line_handler: LineHandler,
209 at_start_of_line: bool = true,
210 error_context_token: ?Token = null,
211 current_code_page: CodePage,
212 default_code_page: CodePage,
213 source_mappings: ?*SourceMappings,
214 max_string_literal_codepoints: u15,
215 /// Needed to determine whether or not the output code page should
216 /// be set in the parser.
217 seen_pragma_code_pages: u2 = 0,
218
219 pub const Error = LexError;
220
221 pub const LexerOptions = struct {
222 default_code_page: CodePage = .windows1252,
223 source_mappings: ?*SourceMappings = null,
224 max_string_literal_codepoints: u15 = default_max_string_literal_codepoints,
225 };
226
227 pub fn init(buffer: []const u8, options: LexerOptions) Self {
228 return Self{
229 .buffer = buffer,
230 .index = 0,
231 .current_code_page = options.default_code_page,
232 .default_code_page = options.default_code_page,
233 .source_mappings = options.source_mappings,
234 .max_string_literal_codepoints = options.max_string_literal_codepoints,
235 .line_handler = .{ .buffer = buffer },
236 };
237 }
238
239 pub fn dump(self: *Self, token: *const Token) void {
240 std.debug.print("{s}:{d}: {s}\n", .{ @tagName(token.id), token.line_number, std.fmt.fmtSliceEscapeLower(token.slice(self.buffer)) });
241 }
242
243 pub const LexMethod = enum {
244 whitespace_delimiter_only,
245 normal,
246 normal_expect_operator,
247 };
248
249 pub fn next(self: *Self, comptime method: LexMethod) LexError!Token {
250 switch (method) {
251 .whitespace_delimiter_only => return self.nextWhitespaceDelimeterOnly(),
252 .normal => return self.nextNormal(),
253 .normal_expect_operator => return self.nextNormalWithContext(.expect_operator),
254 }
255 }
256
257 const StateWhitespaceDelimiterOnly = enum {
258 start,
259 literal,
260 preprocessor,
261 semicolon,
262 };
263
264 pub fn nextWhitespaceDelimeterOnly(self: *Self) LexError!Token {
265 const start_index = self.index;
266 var result = Token{
267 .id = .eof,
268 .start = start_index,
269 .end = undefined,
270 .line_number = self.line_handler.line_number,
271 };
272 var state = StateWhitespaceDelimiterOnly.start;
273
274 while (self.current_code_page.codepointAt(self.index, self.buffer)) |codepoint| : (self.index += codepoint.byte_len) {
275 const c = codepoint.value;
276 try self.checkForIllegalCodepoint(codepoint, false);
277 switch (state) {
278 .start => switch (c) {
279 '\r', '\n' => {
280 result.start = self.index + 1;
281 result.line_number = self.incrementLineNumber();
282 },
283 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
284 result.start = self.index + 1;
285 },
286 // NBSP only counts as whitespace at the start of a line (but
287 // can be intermixed with other whitespace). Who knows why.
288 '\xA0' => if (self.at_start_of_line) {
289 result.start = self.index + codepoint.byte_len;
290 } else {
291 state = .literal;
292 self.at_start_of_line = false;
293 },
294 '#' => {
295 if (self.at_start_of_line) {
296 state = .preprocessor;
297 } else {
298 state = .literal;
299 }
300 self.at_start_of_line = false;
301 },
302 // Semi-colon acts as a line-terminator, but in this lexing mode
303 // that's only true if it's at the start of a line.
304 ';' => {
305 if (self.at_start_of_line) {
306 state = .semicolon;
307 }
308 self.at_start_of_line = false;
309 },
310 else => {
311 state = .literal;
312 self.at_start_of_line = false;
313 },
314 },
315 .literal => switch (c) {
316 '\r', '\n', ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
317 result.id = .literal;
318 break;
319 },
320 else => {},
321 },
322 .preprocessor => switch (c) {
323 '\r', '\n' => {
324 try self.evaluatePreprocessorCommand(result.start, self.index);
325 result.start = self.index + 1;
326 state = .start;
327 result.line_number = self.incrementLineNumber();
328 },
329 else => {},
330 },
331 .semicolon => switch (c) {
332 '\r', '\n' => {
333 result.start = self.index + 1;
334 state = .start;
335 result.line_number = self.incrementLineNumber();
336 },
337 else => {},
338 },
339 }
340 } else { // got EOF
341 switch (state) {
342 .start, .semicolon => {},
343 .literal => {
344 result.id = .literal;
345 },
346 .preprocessor => {
347 try self.evaluatePreprocessorCommand(result.start, self.index);
348 result.start = self.index;
349 },
350 }
351 }
352
353 result.end = self.index;
354 return result;
355 }
356
357 const StateNormal = enum {
358 start,
359 literal_or_quoted_wide_string,
360 quoted_ascii_string,
361 quoted_wide_string,
362 quoted_ascii_string_escape,
363 quoted_wide_string_escape,
364 quoted_ascii_string_maybe_end,
365 quoted_wide_string_maybe_end,
366 literal,
367 number_literal,
368 preprocessor,
369 semicolon,
370 // end
371 e,
372 en,
373 // begin
374 b,
375 be,
376 beg,
377 begi,
378 };
379
380 /// TODO: A not-terrible name
381 pub fn nextNormal(self: *Self) LexError!Token {
382 return self.nextNormalWithContext(.any);
383 }
384
385 pub fn nextNormalWithContext(self: *Self, context: enum { expect_operator, any }) LexError!Token {
386 const start_index = self.index;
387 var result = Token{
388 .id = .eof,
389 .start = start_index,
390 .end = undefined,
391 .line_number = self.line_handler.line_number,
392 };
393 var state = StateNormal.start;
394
395 // Note: The Windows RC compiler uses a non-standard method of computing
396 // length for its 'string literal too long' errors; it isn't easily
397 // explained or intuitive (it's sort-of pre-parsed byte length but with
398 // a few of exceptions/edge cases).
399 //
400 // It also behaves strangely with non-ASCII codepoints, e.g. even though the default
401 // limit is 4097, you can only have 4094 € codepoints (1 UTF-16 code unit each),
402 // and 2048 𐐷 codepoints (2 UTF-16 code units each).
403 //
404 // TODO: Understand this more, bring it more in line with how the Win32 limits work.
405 // Alternatively, do something that makes more sense but may be more permissive.
406 var string_literal_length: usize = 0;
407 var string_literal_collapsing_whitespace: bool = false;
408 var still_could_have_exponent: bool = true;
409 var exponent_index: ?usize = null;
410 while (self.current_code_page.codepointAt(self.index, self.buffer)) |codepoint| : (self.index += codepoint.byte_len) {
411 const c = codepoint.value;
412 const in_string_literal = switch (state) {
413 .quoted_ascii_string,
414 .quoted_wide_string,
415 .quoted_ascii_string_escape,
416 .quoted_wide_string_escape,
417 .quoted_ascii_string_maybe_end,
418 .quoted_wide_string_maybe_end,
419 =>
420 // If the current line is not the same line as the start of the string literal,
421 // then we want to treat the current codepoint as 'not in a string literal'
422 // for the purposes of detecting illegal codepoints. This means that we will
423 // error on illegal-outside-string-literal characters that are outside string
424 // literals from the perspective of a C preprocessor, but that may be
425 // inside string literals from the perspective of the RC lexer. For example,
426 // "hello
427 // @"
428 // will be treated as a single string literal by the RC lexer but the Win32
429 // preprocessor will consider this an unclosed string literal followed by
430 // the character @ and ", and will therefore error since the Win32 RC preprocessor
431 // errors on the @ character outside string literals.
432 //
433 // By doing this here, we can effectively emulate the Win32 RC preprocessor behavior
434 // at lex-time, and avoid the need for a separate step that checks for this edge-case
435 // specifically.
436 result.line_number == self.line_handler.line_number,
437 else => false,
438 };
439 try self.checkForIllegalCodepoint(codepoint, in_string_literal);
440 switch (state) {
441 .start => switch (c) {
442 '\r', '\n' => {
443 result.start = self.index + 1;
444 result.line_number = self.incrementLineNumber();
445 },
446 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
447 result.start = self.index + 1;
448 },
449 // NBSP only counts as whitespace at the start of a line (but
450 // can be intermixed with other whitespace). Who knows why.
451 '\xA0' => if (self.at_start_of_line) {
452 result.start = self.index + codepoint.byte_len;
453 } else {
454 state = .literal;
455 self.at_start_of_line = false;
456 },
457 'L', 'l' => {
458 state = .literal_or_quoted_wide_string;
459 self.at_start_of_line = false;
460 },
461 'E', 'e' => {
462 state = .e;
463 self.at_start_of_line = false;
464 },
465 'B', 'b' => {
466 state = .b;
467 self.at_start_of_line = false;
468 },
469 '"' => {
470 state = .quoted_ascii_string;
471 self.at_start_of_line = false;
472 string_literal_collapsing_whitespace = false;
473 string_literal_length = 0;
474 },
475 '+', '&', '|' => {
476 self.index += 1;
477 result.id = .operator;
478 self.at_start_of_line = false;
479 break;
480 },
481 '-' => {
482 if (context == .expect_operator) {
483 self.index += 1;
484 result.id = .operator;
485 self.at_start_of_line = false;
486 break;
487 } else {
488 state = .number_literal;
489 still_could_have_exponent = true;
490 exponent_index = null;
491 self.at_start_of_line = false;
492 }
493 },
494 '0'...'9', '~' => {
495 state = .number_literal;
496 still_could_have_exponent = true;
497 exponent_index = null;
498 self.at_start_of_line = false;
499 },
500 '#' => {
501 if (self.at_start_of_line) {
502 state = .preprocessor;
503 } else {
504 state = .literal;
505 }
506 self.at_start_of_line = false;
507 },
508 ';' => {
509 state = .semicolon;
510 self.at_start_of_line = false;
511 },
512 '{', '}' => {
513 self.index += 1;
514 result.id = if (c == '{') .begin else .end;
515 self.at_start_of_line = false;
516 break;
517 },
518 '(', ')' => {
519 self.index += 1;
520 result.id = if (c == '(') .open_paren else .close_paren;
521 self.at_start_of_line = false;
522 break;
523 },
524 ',' => {
525 self.index += 1;
526 result.id = .comma;
527 self.at_start_of_line = false;
528 break;
529 },
530 else => {
531 if (isNonAsciiDigit(c)) {
532 self.error_context_token = .{
533 .id = .number,
534 .start = result.start,
535 .end = self.index + 1,
536 .line_number = self.line_handler.line_number,
537 };
538 return error.InvalidDigitCharacterInNumberLiteral;
539 }
540 state = .literal;
541 self.at_start_of_line = false;
542 },
543 },
544 .preprocessor => switch (c) {
545 '\r', '\n' => {
546 try self.evaluatePreprocessorCommand(result.start, self.index);
547 result.start = self.index + 1;
548 state = .start;
549 result.line_number = self.incrementLineNumber();
550 },
551 else => {},
552 },
553 // Semi-colon acts as a line-terminator--everything is skipped until
554 // the next line.
555 .semicolon => switch (c) {
556 '\r', '\n' => {
557 result.start = self.index + 1;
558 state = .start;
559 result.line_number = self.incrementLineNumber();
560 },
561 else => {},
562 },
563 .number_literal => switch (c) {
564 // zig fmt: off
565 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
566 '\r', '\n', '"', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
567 '\'', ';', '=',
568 => {
569 // zig fmt: on
570 result.id = .number;
571 break;
572 },
573 '0'...'9' => {
574 if (exponent_index) |exp_i| {
575 if (self.index - 1 == exp_i) {
576 // Note: This being an error is a quirk of the preprocessor used by
577 // the Win32 RC compiler.
578 self.error_context_token = .{
579 .id = .number,
580 .start = result.start,
581 .end = self.index + 1,
582 .line_number = self.line_handler.line_number,
583 };
584 return error.InvalidNumberWithExponent;
585 }
586 }
587 },
588 'e', 'E' => {
589 if (still_could_have_exponent) {
590 exponent_index = self.index;
591 still_could_have_exponent = false;
592 }
593 },
594 else => {
595 if (isNonAsciiDigit(c)) {
596 self.error_context_token = .{
597 .id = .number,
598 .start = result.start,
599 .end = self.index + 1,
600 .line_number = self.line_handler.line_number,
601 };
602 return error.InvalidDigitCharacterInNumberLiteral;
603 }
604 still_could_have_exponent = false;
605 },
606 },
607 .literal_or_quoted_wide_string => switch (c) {
608 // zig fmt: off
609 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
610 '\r', '\n', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
611 '\'', ';', '=',
612 // zig fmt: on
613 => {
614 result.id = .literal;
615 break;
616 },
617 '"' => {
618 state = .quoted_wide_string;
619 string_literal_collapsing_whitespace = false;
620 string_literal_length = 0;
621 },
622 else => {
623 state = .literal;
624 },
625 },
626 .literal => switch (c) {
627 // zig fmt: off
628 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
629 '\r', '\n', '"', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
630 '\'', ';', '=',
631 => {
632 // zig fmt: on
633 result.id = .literal;
634 break;
635 },
636 else => {},
637 },
638 .e => switch (c) {
639 'N', 'n' => {
640 state = .en;
641 },
642 else => {
643 state = .literal;
644 self.index -= 1;
645 },
646 },
647 .en => switch (c) {
648 'D', 'd' => {
649 result.id = .end;
650 self.index += 1;
651 break;
652 },
653 else => {
654 state = .literal;
655 self.index -= 1;
656 },
657 },
658 .b => switch (c) {
659 'E', 'e' => {
660 state = .be;
661 },
662 else => {
663 state = .literal;
664 self.index -= 1;
665 },
666 },
667 .be => switch (c) {
668 'G', 'g' => {
669 state = .beg;
670 },
671 else => {
672 state = .literal;
673 self.index -= 1;
674 },
675 },
676 .beg => switch (c) {
677 'I', 'i' => {
678 state = .begi;
679 },
680 else => {
681 state = .literal;
682 self.index -= 1;
683 },
684 },
685 .begi => switch (c) {
686 'N', 'n' => {
687 result.id = .begin;
688 self.index += 1;
689 break;
690 },
691 else => {
692 state = .literal;
693 self.index -= 1;
694 },
695 },
696 .quoted_ascii_string, .quoted_wide_string => switch (c) {
697 '"' => {
698 state = if (state == .quoted_ascii_string) .quoted_ascii_string_maybe_end else .quoted_wide_string_maybe_end;
699 },
700 '\\' => {
701 state = if (state == .quoted_ascii_string) .quoted_ascii_string_escape else .quoted_wide_string_escape;
702 },
703 '\r' => {
704 // \r doesn't count towards string literal length
705
706 // Increment line number but don't affect the result token's line number
707 _ = self.incrementLineNumber();
708 },
709 '\n' => {
710 // first \n expands to <space><\n>
711 if (!string_literal_collapsing_whitespace) {
712 string_literal_length += 2;
713 string_literal_collapsing_whitespace = true;
714 }
715 // the rest are collapsed into the <space><\n>
716
717 // Increment line number but don't affect the result token's line number
718 _ = self.incrementLineNumber();
719 },
720 // only \t, space, Vertical Tab, and Form Feed count as whitespace when collapsing
721 '\t', ' ', '\x0b', '\x0c' => {
722 if (!string_literal_collapsing_whitespace) {
723 if (c == '\t') {
724 // Literal tab characters are counted as the number of space characters
725 // needed to reach the next 8-column tab stop.
726 //
727 // This implemention is ineffecient but hopefully it's enough of an
728 // edge case that it doesn't matter too much. Literal tab characters in
729 // string literals being replaced by a variable number of spaces depending
730 // on which column the tab character is located in the source .rc file seems
731 // like it has extremely limited use-cases, so it seems unlikely that it's used
732 // in real .rc files.
733 var dummy_token = Token{
734 .start = self.index,
735 .end = self.index,
736 .line_number = self.line_handler.line_number,
737 .id = .invalid,
738 };
739 dummy_token.start = self.index;
740 const current_column = dummy_token.calculateColumn(self.buffer, 8, null);
741 string_literal_length += columnsUntilTabStop(current_column, 8);
742 } else {
743 string_literal_length += 1;
744 }
745 }
746 },
747 else => {
748 string_literal_collapsing_whitespace = false;
749 string_literal_length += 1;
750 },
751 },
752 .quoted_ascii_string_escape, .quoted_wide_string_escape => switch (c) {
753 '"' => {
754 self.error_context_token = .{
755 .id = .invalid,
756 .start = self.index - 1,
757 .end = self.index + 1,
758 .line_number = self.line_handler.line_number,
759 };
760 return error.FoundCStyleEscapedQuote;
761 },
762 else => {
763 state = if (state == .quoted_ascii_string_escape) .quoted_ascii_string else .quoted_wide_string;
764 },
765 },
766 .quoted_ascii_string_maybe_end, .quoted_wide_string_maybe_end => switch (c) {
767 '"' => {
768 state = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
769 // Escaped quotes only count as 1 char for string literal length checks,
770 // so we don't increment string_literal_length here.
771 },
772 else => {
773 result.id = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
774 break;
775 },
776 },
777 }
778 } else { // got EOF
779 switch (state) {
780 .start, .semicolon => {},
781 .literal_or_quoted_wide_string, .literal, .e, .en, .b, .be, .beg, .begi => {
782 result.id = .literal;
783 },
784 .preprocessor => {
785 try self.evaluatePreprocessorCommand(result.start, self.index);
786 result.start = self.index;
787 },
788 .number_literal => {
789 result.id = .number;
790 },
791 .quoted_ascii_string_maybe_end, .quoted_wide_string_maybe_end => {
792 result.id = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
793 },
794 .quoted_ascii_string,
795 .quoted_wide_string,
796 .quoted_ascii_string_escape,
797 .quoted_wide_string_escape,
798 => {
799 self.error_context_token = .{
800 .id = .eof,
801 .start = self.index,
802 .end = self.index,
803 .line_number = self.line_handler.line_number,
804 };
805 return LexError.UnfinishedStringLiteral;
806 },
807 }
808 }
809
810 if (result.id == .quoted_ascii_string or result.id == .quoted_wide_string) {
811 if (string_literal_length > self.max_string_literal_codepoints) {
812 self.error_context_token = result;
813 return LexError.StringLiteralTooLong;
814 }
815 }
816
817 result.end = self.index;
818 return result;
819 }
820
821 /// Increments line_number appropriately (handling line ending pairs)
822 /// and returns the new line number.
823 fn incrementLineNumber(self: *Self) usize {
824 _ = self.line_handler.incrementLineNumber(self.index);
825 self.at_start_of_line = true;
826 return self.line_handler.line_number;
827 }
828
829 fn checkForIllegalCodepoint(self: *Self, codepoint: code_pages.Codepoint, in_string_literal: bool) LexError!void {
830 const err = switch (codepoint.value) {
831 // 0x00 = NUL
832 // 0x1A = Substitute (treated as EOF)
833 // NOTE: 0x1A gets treated as EOF by the clang preprocessor so after a .rc file
834 // is run through the clang preprocessor it will no longer have 0x1A characters in it.
835 // 0x7F = DEL (treated as a context-specific terminator by the Windows RC compiler)
836 0x00, 0x1A, 0x7F => error.IllegalByte,
837 // 0x01...0x03 result in strange 'macro definition too big' errors when used outside of string literals
838 // 0x04 is valid but behaves strangely (sort of acts as a 'skip the next character' instruction)
839 0x01...0x04 => if (!in_string_literal) error.IllegalByteOutsideStringLiterals else return,
840 // @ and ` both result in error RC2018: unknown character '0x60' (and subsequently
841 // fatal error RC1116: RC terminating after preprocessor errors) if they are ever used
842 // outside of string literals. Not exactly sure why this would be the case, though.
843 // TODO: Make sure there aren't any exceptions
844 '@', '`' => if (!in_string_literal) error.IllegalByteOutsideStringLiterals else return,
845 // The Byte Order Mark is mostly skipped over by the Windows RC compiler, but
846 // there are edge cases where it leads to cryptic 'compiler limit : macro definition too big'
847 // errors (e.g. a BOM within a number literal). By making this illegal we avoid having to
848 // deal with a lot of edge cases and remove the potential footgun of the bytes of a BOM
849 // being 'missing' when included in a string literal (the Windows RC compiler acts as
850 // if the codepoint was never part of the string literal).
851 '\u{FEFF}' => error.IllegalByteOrderMark,
852 // Similar deal with this private use codepoint, it gets skipped/ignored by the
853 // RC compiler (but without the cryptic errors). Silently dropping bytes still seems like
854 // enough of a footgun with no real use-cases that it's still worth erroring instead of
855 // emulating the RC compiler's behavior, though.
856 '\u{E000}' => error.IllegalPrivateUseCharacter,
857 // These codepoints lead to strange errors when used outside of string literals,
858 // and miscompilations when used within string literals. We avoid the miscompilation
859 // within string literals and emit a warning, but outside of string literals it makes
860 // more sense to just disallow these codepoints.
861 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => if (!in_string_literal) error.IllegalCodepointOutsideStringLiterals else return,
862 else => return,
863 };
864 self.error_context_token = .{
865 .id = .invalid,
866 .start = self.index,
867 .end = self.index + codepoint.byte_len,
868 .line_number = self.line_handler.line_number,
869 };
870 return err;
871 }
872
873 fn evaluatePreprocessorCommand(self: *Self, start: usize, end: usize) !void {
874 const token = Token{
875 .id = .preprocessor_command,
876 .start = start,
877 .end = end,
878 .line_number = self.line_handler.line_number,
879 };
880 const full_command = self.buffer[start..end];
881 var command = full_command;
882
883 // Anything besides exactly this is ignored by the Windows RC implementation
884 const expected_directive = "#pragma";
885 if (!std.mem.startsWith(u8, command, expected_directive)) return;
886 command = command[expected_directive.len..];
887
888 if (command.len == 0 or !std.ascii.isWhitespace(command[0])) return;
889 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
890 command = command[1..];
891 }
892
893 // Note: CoDe_PaGeZ is also treated as "code_page" by the Windows RC implementation,
894 // and it will error with 'Missing left parenthesis in code_page #pragma'
895 const expected_extension = "code_page";
896 if (!std.ascii.startsWithIgnoreCase(command, expected_extension)) return;
897 command = command[expected_extension.len..];
898
899 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
900 command = command[1..];
901 }
902
903 if (command.len == 0 or command[0] != '(') {
904 self.error_context_token = token;
905 return error.CodePagePragmaMissingLeftParen;
906 }
907 command = command[1..];
908
909 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
910 command = command[1..];
911 }
912
913 var num_str: []u8 = command[0..0];
914 while (command.len > 0 and (command[0] != ')' and !std.ascii.isWhitespace(command[0]))) {
915 command = command[1..];
916 num_str.len += 1;
917 }
918
919 if (num_str.len == 0) {
920 self.error_context_token = token;
921 return error.CodePagePragmaNotInteger;
922 }
923
924 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
925 command = command[1..];
926 }
927
928 if (command.len == 0 or command[0] != ')') {
929 self.error_context_token = token;
930 return error.CodePagePragmaMissingRightParen;
931 }
932
933 const code_page = code_page: {
934 if (std.ascii.eqlIgnoreCase("DEFAULT", num_str)) {
935 break :code_page self.default_code_page;
936 }
937
938 // The Win32 compiler behaves fairly strangely around maxInt(u32):
939 // - If the overflowed u32 wraps and becomes a known code page ID, then
940 // it will error/warn with "Codepage not valid: ignored" (depending on /w)
941 // - If the overflowed u32 wraps and does not become a known code page ID,
942 // then it will error with 'constant too big' and 'Codepage not integer'
943 //
944 // Instead of that, we just have a separate error specifically for overflow.
945 const num = parseCodePageNum(num_str) catch |err| switch (err) {
946 error.InvalidCharacter => {
947 self.error_context_token = token;
948 return error.CodePagePragmaNotInteger;
949 },
950 error.Overflow => {
951 self.error_context_token = token;
952 return error.CodePagePragmaOverflow;
953 },
954 };
955
956 // Anything that starts with 0 but does not resolve to 0 is treated as invalid, e.g. 01252
957 if (num_str[0] == '0' and num != 0) {
958 self.error_context_token = token;
959 return error.CodePagePragmaInvalidCodePage;
960 }
961 // Anything that resolves to 0 is treated as 'not an integer' by the Win32 implementation.
962 else if (num == 0) {
963 self.error_context_token = token;
964 return error.CodePagePragmaNotInteger;
965 }
966 // Anything above u16 max is not going to be found since our CodePage enum is backed by a u16.
967 if (num > std.math.maxInt(u16)) {
968 self.error_context_token = token;
969 return error.CodePagePragmaInvalidCodePage;
970 }
971
972 break :code_page code_pages.CodePage.getByIdentifierEnsureSupported(@intCast(num)) catch |err| switch (err) {
973 error.InvalidCodePage => {
974 self.error_context_token = token;
975 return error.CodePagePragmaInvalidCodePage;
976 },
977 error.UnsupportedCodePage => {
978 self.error_context_token = token;
979 return error.CodePagePragmaUnsupportedCodePage;
980 },
981 };
982 };
983
984 // https://learn.microsoft.com/en-us/windows/win32/menurc/pragma-directives
985 // > This pragma is not supported in an included resource file (.rc)
986 //
987 // Even though the Win32 behavior is to just ignore such directives silently,
988 // this is an error in the lexer to allow for emitting warnings/errors when
989 // such directives are found if that's wanted. The intention is for the lexer
990 // to still be able to work correctly after this error is returned.
991 if (self.source_mappings) |source_mappings| {
992 if (!source_mappings.isRootFile(token.line_number)) {
993 self.error_context_token = token;
994 return error.CodePagePragmaInIncludedFile;
995 }
996 }
997
998 self.seen_pragma_code_pages +|= 1;
999 self.current_code_page = code_page;
1000 }
1001
1002 fn parseCodePageNum(str: []const u8) !u32 {
1003 var x: u32 = 0;
1004 for (str) |c| {
1005 const digit = try std.fmt.charToDigit(c, 10);
1006 if (x != 0) x = try std.math.mul(u32, x, 10);
1007 x = try std.math.add(u32, x, digit);
1008 }
1009 return x;
1010 }
1011
1012 pub fn getErrorDetails(self: Self, lex_err: LexError) ErrorDetails {
1013 const err = switch (lex_err) {
1014 error.UnfinishedStringLiteral => ErrorDetails.Error.unfinished_string_literal,
1015 error.StringLiteralTooLong => return .{
1016 .err = .string_literal_too_long,
1017 .token = self.error_context_token.?,
1018 .extra = .{ .number = self.max_string_literal_codepoints },
1019 },
1020 error.InvalidNumberWithExponent => ErrorDetails.Error.invalid_number_with_exponent,
1021 error.InvalidDigitCharacterInNumberLiteral => ErrorDetails.Error.invalid_digit_character_in_number_literal,
1022 error.IllegalByte => ErrorDetails.Error.illegal_byte,
1023 error.IllegalByteOutsideStringLiterals => ErrorDetails.Error.illegal_byte_outside_string_literals,
1024 error.IllegalCodepointOutsideStringLiterals => ErrorDetails.Error.illegal_codepoint_outside_string_literals,
1025 error.IllegalByteOrderMark => ErrorDetails.Error.illegal_byte_order_mark,
1026 error.IllegalPrivateUseCharacter => ErrorDetails.Error.illegal_private_use_character,
1027 error.FoundCStyleEscapedQuote => ErrorDetails.Error.found_c_style_escaped_quote,
1028 error.CodePagePragmaMissingLeftParen => ErrorDetails.Error.code_page_pragma_missing_left_paren,
1029 error.CodePagePragmaMissingRightParen => ErrorDetails.Error.code_page_pragma_missing_right_paren,
1030 error.CodePagePragmaInvalidCodePage => ErrorDetails.Error.code_page_pragma_invalid_code_page,
1031 error.CodePagePragmaNotInteger => ErrorDetails.Error.code_page_pragma_not_integer,
1032 error.CodePagePragmaOverflow => ErrorDetails.Error.code_page_pragma_overflow,
1033 error.CodePagePragmaUnsupportedCodePage => ErrorDetails.Error.code_page_pragma_unsupported_code_page,
1034 error.CodePagePragmaInIncludedFile => ErrorDetails.Error.code_page_pragma_in_included_file,
1035 };
1036 return .{
1037 .err = err,
1038 .token = self.error_context_token.?,
1039 };
1040 }
1041};
1042
1043fn testLexNormal(source: []const u8, expected_tokens: []const Token.Id) !void {
1044 var lexer = Lexer.init(source, .{});
1045 if (dumpTokensDuringTests) std.debug.print("\n----------------------\n{s}\n----------------------\n", .{lexer.buffer});
1046 for (expected_tokens) |expected_token_id| {
1047 const token = try lexer.nextNormal();
1048 if (dumpTokensDuringTests) lexer.dump(&token);
1049 try std.testing.expectEqual(expected_token_id, token.id);
1050 }
1051 const last_token = try lexer.nextNormal();
1052 try std.testing.expectEqual(Token.Id.eof, last_token.id);
1053}
1054
1055fn expectLexError(expected: LexError, actual: anytype) !void {
1056 try std.testing.expectError(expected, actual);
1057 if (dumpTokensDuringTests) std.debug.print("{!}\n", .{actual});
1058}
1059
1060test "normal: numbers" {
1061 try testLexNormal("1", &.{.number});
1062 try testLexNormal("-1", &.{.number});
1063 try testLexNormal("- 1", &.{ .number, .number });
1064 try testLexNormal("-a", &.{.number});
1065}
1066
1067test "normal: string literals" {
1068 try testLexNormal("\"\"", &.{.quoted_ascii_string});
1069 // "" is an escaped "
1070 try testLexNormal("\" \"\" \"", &.{.quoted_ascii_string});
1071}
1072
1073test "superscript chars and code pages" {
1074 const firstToken = struct {
1075 pub fn firstToken(source: []const u8, default_code_page: CodePage, comptime lex_method: Lexer.LexMethod) LexError!Token {
1076 var lexer = Lexer.init(source, .{ .default_code_page = default_code_page });
1077 return lexer.next(lex_method);
1078 }
1079 }.firstToken;
1080 const utf8_source = "²";
1081 const windows1252_source = "\xB2";
1082
1083 const windows1252_encoded_as_windows1252 = firstToken(windows1252_source, .windows1252, .normal);
1084 try std.testing.expectError(error.InvalidDigitCharacterInNumberLiteral, windows1252_encoded_as_windows1252);
1085
1086 const utf8_encoded_as_windows1252 = try firstToken(utf8_source, .windows1252, .normal);
1087 try std.testing.expectEqual(Token{
1088 .id = .literal,
1089 .start = 0,
1090 .end = 2,
1091 .line_number = 1,
1092 }, utf8_encoded_as_windows1252);
1093
1094 const utf8_encoded_as_utf8 = firstToken(utf8_source, .utf8, .normal);
1095 try std.testing.expectError(error.InvalidDigitCharacterInNumberLiteral, utf8_encoded_as_utf8);
1096
1097 const windows1252_encoded_as_utf8 = try firstToken(windows1252_source, .utf8, .normal);
1098 try std.testing.expectEqual(Token{
1099 .id = .literal,
1100 .start = 0,
1101 .end = 1,
1102 .line_number = 1,
1103 }, windows1252_encoded_as_utf8);
1104}
src/resinator/literals.zig created+904
......@@ -0,0 +1,904 @@
1const std = @import("std");
2const code_pages = @import("code_pages.zig");
3const CodePage = code_pages.CodePage;
4const windows1252 = @import("windows1252.zig");
5const ErrorDetails = @import("errors.zig").ErrorDetails;
6const DiagnosticsContext = @import("errors.zig").DiagnosticsContext;
7const Token = @import("lex.zig").Token;
8
9/// rc is maximally liberal in terms of what it accepts as a number literal
10/// for data values. As long as it starts with a number or - or ~, that's good enough.
11pub fn isValidNumberDataLiteral(str: []const u8) bool {
12 if (str.len == 0) return false;
13 switch (str[0]) {
14 '~', '-', '0'...'9' => return true,
15 else => return false,
16 }
17}
18
19pub const SourceBytes = struct {
20 slice: []const u8,
21 code_page: CodePage,
22};
23
24pub const StringType = enum { ascii, wide };
25
26/// Valid escapes:
27/// "" -> "
28/// \a, \A => 0x08 (not 0x07 like in C)
29/// \n => 0x0A
30/// \r => 0x0D
31/// \t, \T => 0x09
32/// \\ => \
33/// \nnn => byte with numeric value given by nnn interpreted as octal
34/// (wraps on overflow, number of digits can be 1-3 for ASCII strings
35/// and 1-7 for wide strings)
36/// \xhh => byte with numeric value given by hh interpreted as hex
37/// (number of digits can be 0-2 for ASCII strings and 0-4 for
38/// wide strings)
39/// \<\r+> => \
40/// \<[\r\n\t ]+> => <nothing>
41///
42/// Special cases:
43/// <\t> => 1-8 spaces, dependent on columns in the source rc file itself
44/// <\r> => <nothing>
45/// <\n+><\w+?\n?> => <space><\n>
46///
47/// Special, especially weird case:
48/// \"" => "
49/// NOTE: This leads to footguns because the preprocessor can start parsing things
50/// out-of-sync with the RC compiler, expanding macros within string literals, etc.
51/// This parse function handles this case the same as the Windows RC compiler, but
52/// \" within a string literal is treated as an error by the lexer, so the relevant
53/// branches should never actually be hit during this function.
54pub const IterativeStringParser = struct {
55 source: []const u8,
56 code_page: CodePage,
57 /// The type of the string inferred by the prefix (L"" or "")
58 /// This is what matters for things like the maximum digits in an
59 /// escape sequence, whether or not invalid escape sequences are skipped, etc.
60 declared_string_type: StringType,
61 pending_codepoint: ?u21 = null,
62 num_pending_spaces: u8 = 0,
63 index: usize = 0,
64 column: usize = 0,
65 diagnostics: ?DiagnosticsContext = null,
66 seen_tab: bool = false,
67
68 const State = enum {
69 normal,
70 quote,
71 newline,
72 escaped,
73 escaped_cr,
74 escaped_newlines,
75 escaped_octal,
76 escaped_hex,
77 };
78
79 pub fn init(bytes: SourceBytes, options: StringParseOptions) IterativeStringParser {
80 const declared_string_type: StringType = switch (bytes.slice[0]) {
81 'L', 'l' => .wide,
82 else => .ascii,
83 };
84 var source = bytes.slice[1 .. bytes.slice.len - 1]; // remove ""
85 var column = options.start_column + 1; // for the removed "
86 if (declared_string_type == .wide) {
87 source = source[1..]; // remove L
88 column += 1; // for the removed L
89 }
90 return .{
91 .source = source,
92 .code_page = bytes.code_page,
93 .declared_string_type = declared_string_type,
94 .column = column,
95 .diagnostics = options.diagnostics,
96 };
97 }
98
99 pub const ParsedCodepoint = struct {
100 codepoint: u21,
101 from_escaped_integer: bool = false,
102 };
103
104 pub fn next(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
105 const result = try self.nextUnchecked();
106 if (self.diagnostics != null and result != null and !result.?.from_escaped_integer) {
107 switch (result.?.codepoint) {
108 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => {
109 const err: ErrorDetails.Error = if (result.?.codepoint == 0xD00)
110 .rc_would_miscompile_codepoint_skip
111 else
112 .rc_would_miscompile_codepoint_byte_swap;
113 try self.diagnostics.?.diagnostics.append(ErrorDetails{
114 .err = err,
115 .type = .warning,
116 .token = self.diagnostics.?.token,
117 .extra = .{ .number = result.?.codepoint },
118 });
119 try self.diagnostics.?.diagnostics.append(ErrorDetails{
120 .err = err,
121 .type = .note,
122 .token = self.diagnostics.?.token,
123 .print_source_line = false,
124 .extra = .{ .number = result.?.codepoint },
125 });
126 },
127 else => {},
128 }
129 }
130 return result;
131 }
132
133 pub fn nextUnchecked(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
134 if (self.num_pending_spaces > 0) {
135 // Ensure that we don't get into this predicament so we can ensure that
136 // the order of processing any pending stuff doesn't matter
137 std.debug.assert(self.pending_codepoint == null);
138 self.num_pending_spaces -= 1;
139 return .{ .codepoint = ' ' };
140 }
141 if (self.pending_codepoint) |pending_codepoint| {
142 self.pending_codepoint = null;
143 return .{ .codepoint = pending_codepoint };
144 }
145 if (self.index >= self.source.len) return null;
146
147 var state: State = .normal;
148 var string_escape_n: u16 = 0;
149 var string_escape_i: u8 = 0;
150 const max_octal_escape_digits: u8 = switch (self.declared_string_type) {
151 .ascii => 3,
152 .wide => 7,
153 };
154 const max_hex_escape_digits: u8 = switch (self.declared_string_type) {
155 .ascii => 2,
156 .wide => 4,
157 };
158
159 while (self.code_page.codepointAt(self.index, self.source)) |codepoint| : (self.index += codepoint.byte_len) {
160 const c = codepoint.value;
161 var backtrack = false;
162 defer {
163 if (backtrack) {
164 self.index -= codepoint.byte_len;
165 } else {
166 if (c == '\t') {
167 self.column += columnsUntilTabStop(self.column, 8);
168 } else {
169 self.column += codepoint.byte_len;
170 }
171 }
172 }
173 switch (state) {
174 .normal => switch (c) {
175 '\\' => state = .escaped,
176 '"' => state = .quote,
177 '\r' => {},
178 '\n' => state = .newline,
179 '\t' => {
180 // Only warn about a tab getting converted to spaces once per string
181 if (self.diagnostics != null and !self.seen_tab) {
182 try self.diagnostics.?.diagnostics.append(ErrorDetails{
183 .err = .tab_converted_to_spaces,
184 .type = .warning,
185 .token = self.diagnostics.?.token,
186 });
187 try self.diagnostics.?.diagnostics.append(ErrorDetails{
188 .err = .tab_converted_to_spaces,
189 .type = .note,
190 .token = self.diagnostics.?.token,
191 .print_source_line = false,
192 });
193 self.seen_tab = true;
194 }
195 const cols = columnsUntilTabStop(self.column, 8);
196 self.num_pending_spaces = @intCast(cols - 1);
197 self.index += codepoint.byte_len;
198 return .{ .codepoint = ' ' };
199 },
200 else => {
201 self.index += codepoint.byte_len;
202 return .{ .codepoint = c };
203 },
204 },
205 .quote => switch (c) {
206 '"' => {
207 // "" => "
208 self.index += codepoint.byte_len;
209 return .{ .codepoint = '"' };
210 },
211 else => unreachable, // this is a bug in the lexer
212 },
213 .newline => switch (c) {
214 '\r', ' ', '\t', '\n', '\x0b', '\x0c', '\xa0' => {},
215 else => {
216 // backtrack so that we handle the current char properly
217 backtrack = true;
218 // <space><newline>
219 self.index += codepoint.byte_len;
220 self.pending_codepoint = '\n';
221 return .{ .codepoint = ' ' };
222 },
223 },
224 .escaped => switch (c) {
225 '\r' => state = .escaped_cr,
226 '\n' => state = .escaped_newlines,
227 '0'...'7' => {
228 string_escape_n = std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
229 string_escape_i = 1;
230 state = .escaped_octal;
231 },
232 'x', 'X' => {
233 string_escape_n = 0;
234 string_escape_i = 0;
235 state = .escaped_hex;
236 },
237 else => {
238 switch (c) {
239 'a', 'A' => {
240 self.index += codepoint.byte_len;
241 return .{ .codepoint = '\x08' };
242 }, // might be a bug in RC, but matches its behavior
243 'n' => {
244 self.index += codepoint.byte_len;
245 return .{ .codepoint = '\n' };
246 },
247 'r' => {
248 self.index += codepoint.byte_len;
249 return .{ .codepoint = '\r' };
250 },
251 't', 'T' => {
252 self.index += codepoint.byte_len;
253 return .{ .codepoint = '\t' };
254 },
255 '\\' => {
256 self.index += codepoint.byte_len;
257 return .{ .codepoint = '\\' };
258 },
259 '"' => {
260 // \" is a special case that doesn't get the \ included,
261 backtrack = true;
262 },
263 else => switch (self.declared_string_type) {
264 .wide => {}, // invalid escape sequences are skipped in wide strings
265 .ascii => {
266 // backtrack so that we handle the current char properly
267 backtrack = true;
268 self.index += codepoint.byte_len;
269 return .{ .codepoint = '\\' };
270 },
271 },
272 }
273 state = .normal;
274 },
275 },
276 .escaped_cr => switch (c) {
277 '\r' => {},
278 '\n' => state = .escaped_newlines,
279 else => {
280 // backtrack so that we handle the current char properly
281 backtrack = true;
282 self.index += codepoint.byte_len;
283 return .{ .codepoint = '\\' };
284 },
285 },
286 .escaped_newlines => switch (c) {
287 '\r', '\n', '\t', ' ', '\x0b', '\x0c', '\xa0' => {},
288 else => {
289 // backtrack so that we handle the current char properly
290 backtrack = true;
291 state = .normal;
292 },
293 },
294 .escaped_octal => switch (c) {
295 '0'...'7' => {
296 string_escape_n *%= 8;
297 string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
298 string_escape_i += 1;
299 if (string_escape_i == max_octal_escape_digits) {
300 const escaped_value = switch (self.declared_string_type) {
301 .ascii => @as(u8, @truncate(string_escape_n)),
302 .wide => string_escape_n,
303 };
304 self.index += codepoint.byte_len;
305 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
306 }
307 },
308 else => {
309 // backtrack so that we handle the current char properly
310 backtrack = true;
311 // write out whatever byte we have parsed so far
312 const escaped_value = switch (self.declared_string_type) {
313 .ascii => @as(u8, @truncate(string_escape_n)),
314 .wide => string_escape_n,
315 };
316 self.index += codepoint.byte_len;
317 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
318 },
319 },
320 .escaped_hex => switch (c) {
321 '0'...'9', 'a'...'f', 'A'...'F' => {
322 string_escape_n *= 16;
323 string_escape_n += std.fmt.charToDigit(@intCast(c), 16) catch unreachable;
324 string_escape_i += 1;
325 if (string_escape_i == max_hex_escape_digits) {
326 const escaped_value = switch (self.declared_string_type) {
327 .ascii => @as(u8, @truncate(string_escape_n)),
328 .wide => string_escape_n,
329 };
330 self.index += codepoint.byte_len;
331 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
332 }
333 },
334 else => {
335 // backtrack so that we handle the current char properly
336 backtrack = true;
337 // write out whatever byte we have parsed so far
338 // (even with 0 actual digits, \x alone parses to 0)
339 const escaped_value = switch (self.declared_string_type) {
340 .ascii => @as(u8, @truncate(string_escape_n)),
341 .wide => string_escape_n,
342 };
343 self.index += codepoint.byte_len;
344 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
345 },
346 },
347 }
348 }
349
350 switch (state) {
351 .normal, .escaped_newlines => {},
352 .newline => {
353 // <space><newline>
354 self.pending_codepoint = '\n';
355 return .{ .codepoint = ' ' };
356 },
357 .escaped, .escaped_cr => return .{ .codepoint = '\\' },
358 .escaped_octal, .escaped_hex => {
359 const escaped_value = switch (self.declared_string_type) {
360 .ascii => @as(u8, @truncate(string_escape_n)),
361 .wide => string_escape_n,
362 };
363 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
364 },
365 .quote => unreachable, // this is a bug in the lexer
366 }
367
368 return null;
369 }
370};
371
372pub const StringParseOptions = struct {
373 start_column: usize = 0,
374 diagnostics: ?DiagnosticsContext = null,
375 output_code_page: CodePage = .windows1252,
376};
377
378pub fn parseQuotedString(
379 comptime literal_type: StringType,
380 allocator: std.mem.Allocator,
381 bytes: SourceBytes,
382 options: StringParseOptions,
383) !(switch (literal_type) {
384 .ascii => []u8,
385 .wide => [:0]u16,
386}) {
387 const T = if (literal_type == .ascii) u8 else u16;
388 std.debug.assert(bytes.slice.len >= 2); // must at least have 2 double quote chars
389
390 var buf = try std.ArrayList(T).initCapacity(allocator, bytes.slice.len);
391 errdefer buf.deinit();
392
393 var iterative_parser = IterativeStringParser.init(bytes, options);
394
395 while (try iterative_parser.next()) |parsed| {
396 const c = parsed.codepoint;
397 if (parsed.from_escaped_integer) {
398 try buf.append(@intCast(c));
399 } else {
400 switch (literal_type) {
401 .ascii => switch (options.output_code_page) {
402 .windows1252 => {
403 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
404 try buf.append(best_fit);
405 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
406 try buf.append('?');
407 } else {
408 try buf.appendSlice("??");
409 }
410 },
411 .utf8 => {
412 var codepoint_to_encode = c;
413 if (c == code_pages.Codepoint.invalid) {
414 codepoint_to_encode = '�';
415 }
416 var utf8_buf: [4]u8 = undefined;
417 const utf8_len = std.unicode.utf8Encode(codepoint_to_encode, &utf8_buf) catch unreachable;
418 try buf.appendSlice(utf8_buf[0..utf8_len]);
419 },
420 else => unreachable, // Unsupported code page
421 },
422 .wide => {
423 if (c == code_pages.Codepoint.invalid) {
424 try buf.append(std.mem.nativeToLittle(u16, '�'));
425 } else if (c < 0x10000) {
426 const short: u16 = @intCast(c);
427 try buf.append(std.mem.nativeToLittle(u16, short));
428 } else {
429 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
430 try buf.append(std.mem.nativeToLittle(u16, high));
431 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
432 try buf.append(std.mem.nativeToLittle(u16, low));
433 }
434 },
435 }
436 }
437 }
438
439 if (literal_type == .wide) {
440 return buf.toOwnedSliceSentinel(0);
441 } else {
442 return buf.toOwnedSlice();
443 }
444}
445
446pub fn parseQuotedAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 {
447 std.debug.assert(bytes.slice.len >= 2); // ""
448 return parseQuotedString(.ascii, allocator, bytes, options);
449}
450
451pub fn parseQuotedWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {
452 std.debug.assert(bytes.slice.len >= 3); // L""
453 return parseQuotedString(.wide, allocator, bytes, options);
454}
455
456pub fn parseQuotedStringAsWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {
457 std.debug.assert(bytes.slice.len >= 2); // ""
458 return parseQuotedString(.wide, allocator, bytes, options);
459}
460
461pub fn parseQuotedStringAsAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 {
462 std.debug.assert(bytes.slice.len >= 2); // ""
463 return parseQuotedString(.ascii, allocator, bytes, options);
464}
465
466test "parse quoted ascii string" {
467 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
468 defer arena_allocator.deinit();
469 const arena = arena_allocator.allocator();
470
471 try std.testing.expectEqualSlices(u8, "hello", try parseQuotedAsciiString(arena, .{
472 .slice =
473 \\"hello"
474 ,
475 .code_page = .windows1252,
476 }, .{}));
477 // hex with 0 digits
478 try std.testing.expectEqualSlices(u8, "\x00", try parseQuotedAsciiString(arena, .{
479 .slice =
480 \\"\x"
481 ,
482 .code_page = .windows1252,
483 }, .{}));
484 // hex max of 2 digits
485 try std.testing.expectEqualSlices(u8, "\xFFf", try parseQuotedAsciiString(arena, .{
486 .slice =
487 \\"\XfFf"
488 ,
489 .code_page = .windows1252,
490 }, .{}));
491 // octal with invalid octal digit
492 try std.testing.expectEqualSlices(u8, "\x019", try parseQuotedAsciiString(arena, .{
493 .slice =
494 \\"\19"
495 ,
496 .code_page = .windows1252,
497 }, .{}));
498 // escaped quotes
499 try std.testing.expectEqualSlices(u8, " \" ", try parseQuotedAsciiString(arena, .{
500 .slice =
501 \\" "" "
502 ,
503 .code_page = .windows1252,
504 }, .{}));
505 // backslash right before escaped quotes
506 try std.testing.expectEqualSlices(u8, "\"", try parseQuotedAsciiString(arena, .{
507 .slice =
508 \\"\"""
509 ,
510 .code_page = .windows1252,
511 }, .{}));
512 // octal overflow
513 try std.testing.expectEqualSlices(u8, "\x01", try parseQuotedAsciiString(arena, .{
514 .slice =
515 \\"\401"
516 ,
517 .code_page = .windows1252,
518 }, .{}));
519 // escapes
520 try std.testing.expectEqualSlices(u8, "\x08\n\r\t\\", try parseQuotedAsciiString(arena, .{
521 .slice =
522 \\"\a\n\r\t\\"
523 ,
524 .code_page = .windows1252,
525 }, .{}));
526 // uppercase escapes
527 try std.testing.expectEqualSlices(u8, "\x08\\N\\R\t\\", try parseQuotedAsciiString(arena, .{
528 .slice =
529 \\"\A\N\R\T\\"
530 ,
531 .code_page = .windows1252,
532 }, .{}));
533 // backslash on its own
534 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(arena, .{
535 .slice =
536 \\"\"
537 ,
538 .code_page = .windows1252,
539 }, .{}));
540 // unrecognized escapes
541 try std.testing.expectEqualSlices(u8, "\\b", try parseQuotedAsciiString(arena, .{
542 .slice =
543 \\"\b"
544 ,
545 .code_page = .windows1252,
546 }, .{}));
547 // escaped carriage returns
548 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(
549 arena,
550 .{ .slice = "\"\\\r\r\r\r\r\"", .code_page = .windows1252 },
551 .{},
552 ));
553 // escaped newlines
554 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
555 arena,
556 .{ .slice = "\"\\\n\n\n\n\n\"", .code_page = .windows1252 },
557 .{},
558 ));
559 // escaped CRLF pairs
560 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
561 arena,
562 .{ .slice = "\"\\\r\n\r\n\r\n\r\n\r\n\"", .code_page = .windows1252 },
563 .{},
564 ));
565 // escaped newlines with other whitespace
566 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
567 arena,
568 .{ .slice = "\"\\\n \t\r\n \r\t\n \t\"", .code_page = .windows1252 },
569 .{},
570 ));
571 // literal tab characters get converted to spaces (dependent on source file columns)
572 try std.testing.expectEqualSlices(u8, " ", try parseQuotedAsciiString(
573 arena,
574 .{ .slice = "\"\t\"", .code_page = .windows1252 },
575 .{},
576 ));
577 try std.testing.expectEqualSlices(u8, "abc ", try parseQuotedAsciiString(
578 arena,
579 .{ .slice = "\"abc\t\"", .code_page = .windows1252 },
580 .{},
581 ));
582 try std.testing.expectEqualSlices(u8, "abcdefg ", try parseQuotedAsciiString(
583 arena,
584 .{ .slice = "\"abcdefg\t\"", .code_page = .windows1252 },
585 .{},
586 ));
587 try std.testing.expectEqualSlices(u8, "\\ ", try parseQuotedAsciiString(
588 arena,
589 .{ .slice = "\"\\\t\"", .code_page = .windows1252 },
590 .{},
591 ));
592 // literal CR's get dropped
593 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
594 arena,
595 .{ .slice = "\"\r\r\r\r\r\"", .code_page = .windows1252 },
596 .{},
597 ));
598 // contiguous newlines and whitespace get collapsed to <space><newline>
599 try std.testing.expectEqualSlices(u8, " \n", try parseQuotedAsciiString(
600 arena,
601 .{ .slice = "\"\n\r\r \r\n \t \"", .code_page = .windows1252 },
602 .{},
603 ));
604}
605
606test "parse quoted ascii string with utf8 code page" {
607 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
608 defer arena_allocator.deinit();
609 const arena = arena_allocator.allocator();
610
611 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
612 arena,
613 .{ .slice = "\"\"", .code_page = .utf8 },
614 .{},
615 ));
616 // Codepoints that don't have a Windows-1252 representation get converted to ?
617 try std.testing.expectEqualSlices(u8, "?????????", try parseQuotedAsciiString(
618 arena,
619 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
620 .{},
621 ));
622 // Codepoints that have a best fit mapping get converted accordingly,
623 // these are box drawing codepoints
624 try std.testing.expectEqualSlices(u8, "\x2b\x2d\x2b", try parseQuotedAsciiString(
625 arena,
626 .{ .slice = "\"┌─┐\"", .code_page = .utf8 },
627 .{},
628 ));
629 // Invalid UTF-8 gets converted to ? depending on well-formedness
630 try std.testing.expectEqualSlices(u8, "????", try parseQuotedAsciiString(
631 arena,
632 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
633 .{},
634 ));
635 // Codepoints that would require a UTF-16 surrogate pair get converted to ??
636 try std.testing.expectEqualSlices(u8, "??", try parseQuotedAsciiString(
637 arena,
638 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
639 .{},
640 ));
641
642 // Output code page changes how invalid UTF-8 gets converted, since it
643 // now encodes the result as UTF-8 so it can write replacement characters.
644 try std.testing.expectEqualSlices(u8, "����", try parseQuotedAsciiString(
645 arena,
646 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
647 .{ .output_code_page = .utf8 },
648 ));
649 try std.testing.expectEqualSlices(u8, "\xF2\xAF\xBA\xB4", try parseQuotedAsciiString(
650 arena,
651 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
652 .{ .output_code_page = .utf8 },
653 ));
654}
655
656test "parse quoted wide string" {
657 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
658 defer arena_allocator.deinit();
659 const arena = arena_allocator.allocator();
660
661 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ 'h', 'e', 'l', 'l', 'o' }, try parseQuotedWideString(arena, .{
662 .slice =
663 \\L"hello"
664 ,
665 .code_page = .windows1252,
666 }, .{}));
667 // hex with 0 digits
668 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{0x0}, try parseQuotedWideString(arena, .{
669 .slice =
670 \\L"\x"
671 ,
672 .code_page = .windows1252,
673 }, .{}));
674 // hex max of 4 digits
675 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ 0xFFFF, 'f' }, try parseQuotedWideString(arena, .{
676 .slice =
677 \\L"\XfFfFf"
678 ,
679 .code_page = .windows1252,
680 }, .{}));
681 // octal max of 7 digits
682 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ 0x9493, '3', '3' }, try parseQuotedWideString(arena, .{
683 .slice =
684 \\L"\111222333"
685 ,
686 .code_page = .windows1252,
687 }, .{}));
688 // octal overflow
689 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{0xFF01}, try parseQuotedWideString(arena, .{
690 .slice =
691 \\L"\777401"
692 ,
693 .code_page = .windows1252,
694 }, .{}));
695 // literal tab characters get converted to spaces (dependent on source file columns)
696 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("abcdefg "), try parseQuotedWideString(
697 arena,
698 .{ .slice = "L\"abcdefg\t\"", .code_page = .windows1252 },
699 .{},
700 ));
701 // Windows-1252 conversion
702 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("ðð€€€"), try parseQuotedWideString(
703 arena,
704 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .windows1252 },
705 .{},
706 ));
707 // Invalid escape sequences are skipped
708 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedWideString(
709 arena,
710 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
711 .{},
712 ));
713}
714
715test "parse quoted wide string with utf8 code page" {
716 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
717 defer arena_allocator.deinit();
718 const arena = arena_allocator.allocator();
719
720 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{}, try parseQuotedWideString(
721 arena,
722 .{ .slice = "L\"\"", .code_page = .utf8 },
723 .{},
724 ));
725 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedWideString(
726 arena,
727 .{ .slice = "L\"кириллица\"", .code_page = .utf8 },
728 .{},
729 ));
730 // Invalid UTF-8 gets converted to � depending on well-formedness
731 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("����"), try parseQuotedWideString(
732 arena,
733 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
734 .{},
735 ));
736}
737
738test "parse quoted ascii string as wide string" {
739 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
740 defer arena_allocator.deinit();
741 const arena = arena_allocator.allocator();
742
743 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedStringAsWideString(
744 arena,
745 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
746 .{},
747 ));
748 // Whether or not invalid escapes are skipped is still determined by the L prefix
749 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("\\H"), try parseQuotedStringAsWideString(
750 arena,
751 .{ .slice = "\"\\H\"", .code_page = .windows1252 },
752 .{},
753 ));
754 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedStringAsWideString(
755 arena,
756 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
757 .{},
758 ));
759 // Maximum escape sequence value is also determined by the L prefix
760 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("\x1234"), try parseQuotedStringAsWideString(
761 arena,
762 .{ .slice = "\"\\x1234\"", .code_page = .windows1252 },
763 .{},
764 ));
765 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{0x1234}, try parseQuotedStringAsWideString(
766 arena,
767 .{ .slice = "L\"\\x1234\"", .code_page = .windows1252 },
768 .{},
769 ));
770}
771
772pub fn columnsUntilTabStop(column: usize, tab_columns: usize) usize {
773 // 0 => 8, 1 => 7, 2 => 6, 3 => 5, 4 => 4
774 // 5 => 3, 6 => 2, 7 => 1, 8 => 8
775 return tab_columns - (column % tab_columns);
776}
777
778pub const Number = struct {
779 value: u32,
780 is_long: bool = false,
781
782 pub fn asWord(self: Number) u16 {
783 return @truncate(self.value);
784 }
785
786 pub fn evaluateOperator(lhs: Number, operator_char: u8, rhs: Number) Number {
787 const result = switch (operator_char) {
788 '-' => lhs.value -% rhs.value,
789 '+' => lhs.value +% rhs.value,
790 '|' => lhs.value | rhs.value,
791 '&' => lhs.value & rhs.value,
792 else => unreachable, // invalid operator, this would be a lexer/parser bug
793 };
794 return .{
795 .value = result,
796 .is_long = lhs.is_long or rhs.is_long,
797 };
798 }
799};
800
801/// Assumes that number literals normally rejected by RC's preprocessor
802/// are similarly rejected before being parsed.
803///
804/// Relevant RC preprocessor errors:
805/// RC2021: expected exponent value, not '<digit>'
806/// example that is rejected: 1e1
807/// example that is accepted: 1ea
808/// (this function will parse the two examples above the same)
809pub fn parseNumberLiteral(bytes: SourceBytes) Number {
810 std.debug.assert(bytes.slice.len > 0);
811 var result = Number{ .value = 0, .is_long = false };
812 var radix: u8 = 10;
813 var buf = bytes.slice;
814
815 const Prefix = enum { none, minus, complement };
816 var prefix: Prefix = .none;
817 switch (buf[0]) {
818 '-' => {
819 prefix = .minus;
820 buf = buf[1..];
821 },
822 '~' => {
823 prefix = .complement;
824 buf = buf[1..];
825 },
826 else => {},
827 }
828
829 if (buf.len > 2 and buf[0] == '0') {
830 switch (buf[1]) {
831 'o' => { // octal radix prefix is case-sensitive
832 radix = 8;
833 buf = buf[2..];
834 },
835 'x', 'X' => {
836 radix = 16;
837 buf = buf[2..];
838 },
839 else => {},
840 }
841 }
842
843 var i: usize = 0;
844 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
845 const c = codepoint.value;
846 if (c == 'L' or c == 'l') {
847 result.is_long = true;
848 break;
849 }
850 const digit = switch (c) {
851 // On invalid digit for the radix, just stop parsing but don't fail
852 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch break,
853 else => break,
854 };
855
856 if (result.value != 0) {
857 result.value *%= radix;
858 }
859 result.value +%= digit;
860 }
861
862 switch (prefix) {
863 .none => {},
864 .minus => result.value = 0 -% result.value,
865 .complement => result.value = ~result.value,
866 }
867
868 return result;
869}
870
871test "parse number literal" {
872 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "0", .code_page = .windows1252 }));
873 try std.testing.expectEqual(Number{ .value = 1, .is_long = false }, parseNumberLiteral(.{ .slice = "1", .code_page = .windows1252 }));
874 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "1L", .code_page = .windows1252 }));
875 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "1l", .code_page = .windows1252 }));
876 try std.testing.expectEqual(Number{ .value = 1, .is_long = false }, parseNumberLiteral(.{ .slice = "1garbageL", .code_page = .windows1252 }));
877 try std.testing.expectEqual(Number{ .value = 4294967295, .is_long = false }, parseNumberLiteral(.{ .slice = "4294967295", .code_page = .windows1252 }));
878 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "4294967296", .code_page = .windows1252 }));
879 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "4294967297L", .code_page = .windows1252 }));
880
881 // can handle any length of number, wraps on overflow appropriately
882 const big_overflow = parseNumberLiteral(.{ .slice = "1000000000000000000000000000000000000000000000000000000000000000000000000000000090000000001", .code_page = .windows1252 });
883 try std.testing.expectEqual(Number{ .value = 4100654081, .is_long = false }, big_overflow);
884 try std.testing.expectEqual(@as(u16, 1025), big_overflow.asWord());
885
886 try std.testing.expectEqual(Number{ .value = 0x20, .is_long = false }, parseNumberLiteral(.{ .slice = "0x20", .code_page = .windows1252 }));
887 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2AL", .code_page = .windows1252 }));
888 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL", .code_page = .windows1252 }));
889 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL", .code_page = .windows1252 }));
890
891 try std.testing.expectEqual(Number{ .value = 0o20, .is_long = false }, parseNumberLiteral(.{ .slice = "0o20", .code_page = .windows1252 }));
892 try std.testing.expectEqual(Number{ .value = 0o20, .is_long = true }, parseNumberLiteral(.{ .slice = "0o20L", .code_page = .windows1252 }));
893 try std.testing.expectEqual(Number{ .value = 0o2, .is_long = false }, parseNumberLiteral(.{ .slice = "0o29", .code_page = .windows1252 }));
894 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "0O29", .code_page = .windows1252 }));
895
896 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFF, .is_long = false }, parseNumberLiteral(.{ .slice = "-1", .code_page = .windows1252 }));
897 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFE, .is_long = false }, parseNumberLiteral(.{ .slice = "~1", .code_page = .windows1252 }));
898 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFF, .is_long = true }, parseNumberLiteral(.{ .slice = "-4294967297L", .code_page = .windows1252 }));
899 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFE, .is_long = true }, parseNumberLiteral(.{ .slice = "~4294967297L", .code_page = .windows1252 }));
900 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFD, .is_long = false }, parseNumberLiteral(.{ .slice = "-0X3", .code_page = .windows1252 }));
901
902 // anything after L is ignored
903 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL5", .code_page = .windows1252 }));
904}
src/resinator/parse.zig created+1880
......@@ -0,0 +1,1880 @@
1const std = @import("std");
2const Lexer = @import("lex.zig").Lexer;
3const Token = @import("lex.zig").Token;
4const Node = @import("ast.zig").Node;
5const Tree = @import("ast.zig").Tree;
6const CodePageLookup = @import("ast.zig").CodePageLookup;
7const Resource = @import("rc.zig").Resource;
8const Allocator = std.mem.Allocator;
9const ErrorDetails = @import("errors.zig").ErrorDetails;
10const Diagnostics = @import("errors.zig").Diagnostics;
11const SourceBytes = @import("literals.zig").SourceBytes;
12const Compiler = @import("compile.zig").Compiler;
13const rc = @import("rc.zig");
14const res = @import("res.zig");
15
16// TODO: Make these configurable?
17pub const max_nested_menu_level: u32 = 512;
18pub const max_nested_version_level: u32 = 512;
19pub const max_nested_expression_level: u32 = 200;
20
21pub const Parser = struct {
22 const Self = @This();
23
24 lexer: *Lexer,
25 /// values that need to be initialized per-parse
26 state: Parser.State = undefined,
27 options: Parser.Options,
28
29 pub const Error = error{ParseError} || Allocator.Error;
30
31 pub const Options = struct {
32 warn_instead_of_error_on_invalid_code_page: bool = false,
33 };
34
35 pub fn init(lexer: *Lexer, options: Options) Parser {
36 return Parser{
37 .lexer = lexer,
38 .options = options,
39 };
40 }
41
42 pub const State = struct {
43 token: Token,
44 lookahead_lexer: Lexer,
45 allocator: Allocator,
46 arena: Allocator,
47 diagnostics: *Diagnostics,
48 input_code_page_lookup: CodePageLookup,
49 output_code_page_lookup: CodePageLookup,
50 };
51
52 pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree {
53 var arena = std.heap.ArenaAllocator.init(allocator);
54 errdefer arena.deinit();
55
56 self.state = Parser.State{
57 .token = undefined,
58 .lookahead_lexer = undefined,
59 .allocator = allocator,
60 .arena = arena.allocator(),
61 .diagnostics = diagnostics,
62 .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
63 .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
64 };
65
66 const parsed_root = try self.parseRoot();
67
68 const tree = try self.state.arena.create(Tree);
69 tree.* = .{
70 .node = parsed_root,
71 .input_code_pages = self.state.input_code_page_lookup,
72 .output_code_pages = self.state.output_code_page_lookup,
73 .source = self.lexer.buffer,
74 .arena = arena.state,
75 .allocator = allocator,
76 };
77 return tree;
78 }
79
80 fn parseRoot(self: *Self) Error!*Node {
81 var statements = std.ArrayList(*Node).init(self.state.allocator);
82 defer statements.deinit();
83
84 try self.parseStatements(&statements);
85 try self.check(.eof);
86
87 const node = try self.state.arena.create(Node.Root);
88 node.* = .{
89 .body = try self.state.arena.dupe(*Node, statements.items),
90 };
91 return &node.base;
92 }
93
94 fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void {
95 while (true) {
96 try self.nextToken(.whitespace_delimiter_only);
97 if (self.state.token.id == .eof) break;
98 // The Win32 compiler will sometimes try to recover from errors
99 // and then restart parsing afterwards. We don't ever do this
100 // because it almost always leads to unhelpful error messages
101 // (usually it will end up with bogus things like 'file
102 // not found: {')
103 var statement = try self.parseStatement();
104 try statements.append(statement);
105 }
106 }
107
108 /// Expects the current token to be the token before possible common resource attributes.
109 /// After return, the current token will be the token immediately before the end of the
110 /// common resource attributes (if any). If there are no common resource attributes, the
111 /// current token is unchanged.
112 /// The returned slice is allocated by the parser's arena
113 fn parseCommonResourceAttributes(self: *Self) ![]Token {
114 var common_resource_attributes = std.ArrayListUnmanaged(Token){};
115 while (true) {
116 const maybe_common_resource_attribute = try self.lookaheadToken(.normal);
117 if (maybe_common_resource_attribute.id == .literal and rc.CommonResourceAttributes.map.has(maybe_common_resource_attribute.slice(self.lexer.buffer))) {
118 try common_resource_attributes.append(self.state.arena, maybe_common_resource_attribute);
119 self.nextToken(.normal) catch unreachable;
120 } else {
121 break;
122 }
123 }
124 return common_resource_attributes.toOwnedSlice(self.state.arena);
125 }
126
127 /// Expects the current token to have already been dealt with, and that the
128 /// optional statements will potentially start on the next token.
129 /// After return, the current token will be the token immediately before the end of the
130 /// optional statements (if any). If there are no optional statements, the
131 /// current token is unchanged.
132 /// The returned slice is allocated by the parser's arena
133 fn parseOptionalStatements(self: *Self, resource: Resource) ![]*Node {
134 var optional_statements = std.ArrayListUnmanaged(*Node){};
135 while (true) {
136 const lookahead_token = try self.lookaheadToken(.normal);
137 if (lookahead_token.id != .literal) break;
138 const slice = lookahead_token.slice(self.lexer.buffer);
139 const optional_statement_type = rc.OptionalStatements.map.get(slice) orelse switch (resource) {
140 .dialog, .dialogex => rc.OptionalStatements.dialog_map.get(slice) orelse break,
141 else => break,
142 };
143 self.nextToken(.normal) catch unreachable;
144 switch (optional_statement_type) {
145 .language => {
146 const language = try self.parseLanguageStatement();
147 try optional_statements.append(self.state.arena, language);
148 },
149 // Number only
150 .version, .characteristics, .style, .exstyle => {
151 const identifier = self.state.token;
152 const value = try self.parseExpression(.{
153 .can_contain_not_expressions = optional_statement_type == .style or optional_statement_type == .exstyle,
154 .allowed_types = .{ .number = true },
155 });
156 const node = try self.state.arena.create(Node.SimpleStatement);
157 node.* = .{
158 .identifier = identifier,
159 .value = value,
160 };
161 try optional_statements.append(self.state.arena, &node.base);
162 },
163 // String only
164 .caption => {
165 const identifier = self.state.token;
166 try self.nextToken(.normal);
167 const value = self.state.token;
168 if (!value.isStringLiteral()) {
169 return self.addErrorDetailsAndFail(ErrorDetails{
170 .err = .expected_something_else,
171 .token = value,
172 .extra = .{ .expected_types = .{
173 .string_literal = true,
174 } },
175 });
176 }
177 // TODO: Wrapping this in a Node.Literal is superfluous but necessary
178 // to put it in a SimpleStatement
179 const value_node = try self.state.arena.create(Node.Literal);
180 value_node.* = .{
181 .token = value,
182 };
183 const node = try self.state.arena.create(Node.SimpleStatement);
184 node.* = .{
185 .identifier = identifier,
186 .value = &value_node.base,
187 };
188 try optional_statements.append(self.state.arena, &node.base);
189 },
190 // String or number
191 .class => {
192 const identifier = self.state.token;
193 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
194 const node = try self.state.arena.create(Node.SimpleStatement);
195 node.* = .{
196 .identifier = identifier,
197 .value = value,
198 };
199 try optional_statements.append(self.state.arena, &node.base);
200 },
201 // Special case
202 .menu => {
203 const identifier = self.state.token;
204 try self.nextToken(.whitespace_delimiter_only);
205 try self.check(.literal);
206 // TODO: Wrapping this in a Node.Literal is superfluous but necessary
207 // to put it in a SimpleStatement
208 const value_node = try self.state.arena.create(Node.Literal);
209 value_node.* = .{
210 .token = self.state.token,
211 };
212 const node = try self.state.arena.create(Node.SimpleStatement);
213 node.* = .{
214 .identifier = identifier,
215 .value = &value_node.base,
216 };
217 try optional_statements.append(self.state.arena, &node.base);
218 },
219 .font => {
220 const identifier = self.state.token;
221 const point_size = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
222
223 // The comma between point_size and typeface is both optional and
224 // there can be any number of them
225 try self.skipAnyCommas();
226
227 try self.nextToken(.normal);
228 const typeface = self.state.token;
229 if (!typeface.isStringLiteral()) {
230 return self.addErrorDetailsAndFail(ErrorDetails{
231 .err = .expected_something_else,
232 .token = typeface,
233 .extra = .{ .expected_types = .{
234 .string_literal = true,
235 } },
236 });
237 }
238
239 const ExSpecificValues = struct {
240 weight: ?*Node = null,
241 italic: ?*Node = null,
242 char_set: ?*Node = null,
243 };
244 var ex_specific = ExSpecificValues{};
245 ex_specific: {
246 var optional_param_parser = OptionalParamParser{ .parser = self };
247 switch (resource) {
248 .dialogex => {
249 {
250 ex_specific.weight = try optional_param_parser.parse(.{});
251 if (optional_param_parser.finished) break :ex_specific;
252 }
253 {
254 if (!(try self.parseOptionalToken(.comma))) break :ex_specific;
255 ex_specific.italic = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
256 }
257 {
258 ex_specific.char_set = try optional_param_parser.parse(.{});
259 if (optional_param_parser.finished) break :ex_specific;
260 }
261 },
262 .dialog => {},
263 else => unreachable, // only DIALOG and DIALOGEX have FONT optional-statements
264 }
265 }
266
267 const node = try self.state.arena.create(Node.FontStatement);
268 node.* = .{
269 .identifier = identifier,
270 .point_size = point_size,
271 .typeface = typeface,
272 .weight = ex_specific.weight,
273 .italic = ex_specific.italic,
274 .char_set = ex_specific.char_set,
275 };
276 try optional_statements.append(self.state.arena, &node.base);
277 },
278 }
279 }
280 return optional_statements.toOwnedSlice(self.state.arena);
281 }
282
283 /// Expects the current token to be the first token of the statement.
284 fn parseStatement(self: *Self) Error!*Node {
285 const first_token = self.state.token;
286 std.debug.assert(first_token.id == .literal);
287
288 if (rc.TopLevelKeywords.map.get(first_token.slice(self.lexer.buffer))) |keyword| switch (keyword) {
289 .language => {
290 const language_statement = try self.parseLanguageStatement();
291 return language_statement;
292 },
293 .version, .characteristics => {
294 const identifier = self.state.token;
295 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
296 const node = try self.state.arena.create(Node.SimpleStatement);
297 node.* = .{
298 .identifier = identifier,
299 .value = value,
300 };
301 return &node.base;
302 },
303 .stringtable => {
304 // common resource attributes must all be contiguous and come before optional-statements
305 const common_resource_attributes = try self.parseCommonResourceAttributes();
306 const optional_statements = try self.parseOptionalStatements(.stringtable);
307
308 try self.nextToken(.normal);
309 const begin_token = self.state.token;
310 try self.check(.begin);
311
312 var strings = std.ArrayList(*Node).init(self.state.allocator);
313 defer strings.deinit();
314 while (true) {
315 const maybe_end_token = try self.lookaheadToken(.normal);
316 switch (maybe_end_token.id) {
317 .end => {
318 self.nextToken(.normal) catch unreachable;
319 break;
320 },
321 .eof => {
322 return self.addErrorDetailsAndFail(ErrorDetails{
323 .err = .unfinished_string_table_block,
324 .token = maybe_end_token,
325 });
326 },
327 else => {},
328 }
329 const id_expression = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
330
331 const comma_token: ?Token = if (try self.parseOptionalToken(.comma)) self.state.token else null;
332
333 try self.nextToken(.normal);
334 if (self.state.token.id != .quoted_ascii_string and self.state.token.id != .quoted_wide_string) {
335 return self.addErrorDetailsAndFail(ErrorDetails{
336 .err = .expected_something_else,
337 .token = self.state.token,
338 .extra = .{ .expected_types = .{ .string_literal = true } },
339 });
340 }
341
342 const string_node = try self.state.arena.create(Node.StringTableString);
343 string_node.* = .{
344 .id = id_expression,
345 .maybe_comma = comma_token,
346 .string = self.state.token,
347 };
348 try strings.append(&string_node.base);
349 }
350
351 if (strings.items.len == 0) {
352 return self.addErrorDetailsAndFail(ErrorDetails{
353 .err = .expected_token, // TODO: probably a more specific error message
354 .token = self.state.token,
355 .extra = .{ .expected = .number },
356 });
357 }
358
359 const end_token = self.state.token;
360 try self.check(.end);
361
362 const node = try self.state.arena.create(Node.StringTable);
363 node.* = .{
364 .type = first_token,
365 .common_resource_attributes = common_resource_attributes,
366 .optional_statements = optional_statements,
367 .begin_token = begin_token,
368 .strings = try self.state.arena.dupe(*Node, strings.items),
369 .end_token = end_token,
370 };
371 return &node.base;
372 },
373 };
374
375 // The Win32 RC compiler allows for a 'dangling' literal at the end of a file
376 // (as long as it's not a valid top-level keyword), and there is actually an
377 // .rc file with a such a dangling literal in the Windows-classic-samples set
378 // of projects. So, we have special compatibility for this particular case.
379 const maybe_eof = try self.lookaheadToken(.whitespace_delimiter_only);
380 if (maybe_eof.id == .eof) {
381 // TODO: emit warning
382 var context = try self.state.arena.alloc(Token, 2);
383 context[0] = first_token;
384 context[1] = maybe_eof;
385 const invalid_node = try self.state.arena.create(Node.Invalid);
386 invalid_node.* = .{
387 .context = context,
388 };
389 return &invalid_node.base;
390 }
391
392 const id_token = first_token;
393 const id_code_page = self.lexer.current_code_page;
394 try self.nextToken(.whitespace_delimiter_only);
395 const resource = try self.checkResource();
396 const type_token = self.state.token;
397
398 if (resource == .string_num) {
399 try self.addErrorDetails(.{
400 .err = .string_resource_as_numeric_type,
401 .token = type_token,
402 });
403 return self.addErrorDetailsAndFail(.{
404 .err = .string_resource_as_numeric_type,
405 .token = type_token,
406 .type = .note,
407 .print_source_line = false,
408 });
409 }
410
411 if (resource == .font) {
412 const id_bytes = SourceBytes{
413 .slice = id_token.slice(self.lexer.buffer),
414 .code_page = id_code_page,
415 };
416 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(id_bytes);
417 if (maybe_ordinal == null) {
418 const would_be_win32_rc_ordinal = res.NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes);
419 if (would_be_win32_rc_ordinal) |win32_rc_ordinal| {
420 try self.addErrorDetails(ErrorDetails{
421 .err = .id_must_be_ordinal,
422 .token = id_token,
423 .extra = .{ .resource = resource },
424 });
425 return self.addErrorDetailsAndFail(ErrorDetails{
426 .err = .win32_non_ascii_ordinal,
427 .token = id_token,
428 .type = .note,
429 .print_source_line = false,
430 .extra = .{ .number = win32_rc_ordinal.ordinal },
431 });
432 } else {
433 return self.addErrorDetailsAndFail(ErrorDetails{
434 .err = .id_must_be_ordinal,
435 .token = id_token,
436 .extra = .{ .resource = resource },
437 });
438 }
439 }
440 }
441
442 switch (resource) {
443 .accelerators => {
444 // common resource attributes must all be contiguous and come before optional-statements
445 const common_resource_attributes = try self.parseCommonResourceAttributes();
446 const optional_statements = try self.parseOptionalStatements(resource);
447
448 try self.nextToken(.normal);
449 const begin_token = self.state.token;
450 try self.check(.begin);
451
452 var accelerators = std.ArrayListUnmanaged(*Node){};
453
454 while (true) {
455 const lookahead = try self.lookaheadToken(.normal);
456 switch (lookahead.id) {
457 .end, .eof => {
458 self.nextToken(.normal) catch unreachable;
459 break;
460 },
461 else => {},
462 }
463 const event = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
464
465 try self.nextToken(.normal);
466 try self.check(.comma);
467
468 const idvalue = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
469
470 var type_and_options = std.ArrayListUnmanaged(Token){};
471 while (true) {
472 if (!(try self.parseOptionalToken(.comma))) break;
473
474 try self.nextToken(.normal);
475 if (!rc.AcceleratorTypeAndOptions.map.has(self.tokenSlice())) {
476 return self.addErrorDetailsAndFail(.{
477 .err = .expected_something_else,
478 .token = self.state.token,
479 .extra = .{ .expected_types = .{
480 .accelerator_type_or_option = true,
481 } },
482 });
483 }
484 try type_and_options.append(self.state.arena, self.state.token);
485 }
486
487 const node = try self.state.arena.create(Node.Accelerator);
488 node.* = .{
489 .event = event,
490 .idvalue = idvalue,
491 .type_and_options = try type_and_options.toOwnedSlice(self.state.arena),
492 };
493 try accelerators.append(self.state.arena, &node.base);
494 }
495
496 const end_token = self.state.token;
497 try self.check(.end);
498
499 const node = try self.state.arena.create(Node.Accelerators);
500 node.* = .{
501 .id = id_token,
502 .type = type_token,
503 .common_resource_attributes = common_resource_attributes,
504 .optional_statements = optional_statements,
505 .begin_token = begin_token,
506 .accelerators = try accelerators.toOwnedSlice(self.state.arena),
507 .end_token = end_token,
508 };
509 return &node.base;
510 },
511 .dialog, .dialogex => {
512 // common resource attributes must all be contiguous and come before optional-statements
513 const common_resource_attributes = try self.parseCommonResourceAttributes();
514
515 const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
516 _ = try self.parseOptionalToken(.comma);
517
518 const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
519 _ = try self.parseOptionalToken(.comma);
520
521 const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
522 _ = try self.parseOptionalToken(.comma);
523
524 const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
525
526 var optional_param_parser = OptionalParamParser{ .parser = self };
527 const help_id: ?*Node = try optional_param_parser.parse(.{});
528
529 const optional_statements = try self.parseOptionalStatements(resource);
530
531 try self.nextToken(.normal);
532 const begin_token = self.state.token;
533 try self.check(.begin);
534
535 var controls = std.ArrayListUnmanaged(*Node){};
536 defer controls.deinit(self.state.allocator);
537 while (try self.parseControlStatement(resource)) |control_node| {
538 // The number of controls must fit in a u16 in order for it to
539 // be able to be written into the relevant field in the .res data.
540 if (controls.items.len >= std.math.maxInt(u16)) {
541 try self.addErrorDetails(.{
542 .err = .too_many_dialog_controls,
543 .token = id_token,
544 .extra = .{ .resource = resource },
545 });
546 return self.addErrorDetailsAndFail(.{
547 .err = .too_many_dialog_controls,
548 .type = .note,
549 .token = control_node.getFirstToken(),
550 .token_span_end = control_node.getLastToken(),
551 .extra = .{ .resource = resource },
552 });
553 }
554
555 try controls.append(self.state.allocator, control_node);
556 }
557
558 try self.nextToken(.normal);
559 const end_token = self.state.token;
560 try self.check(.end);
561
562 const node = try self.state.arena.create(Node.Dialog);
563 node.* = .{
564 .id = id_token,
565 .type = type_token,
566 .common_resource_attributes = common_resource_attributes,
567 .x = x,
568 .y = y,
569 .width = width,
570 .height = height,
571 .help_id = help_id,
572 .optional_statements = optional_statements,
573 .begin_token = begin_token,
574 .controls = try self.state.arena.dupe(*Node, controls.items),
575 .end_token = end_token,
576 };
577 return &node.base;
578 },
579 .toolbar => {
580 // common resource attributes must all be contiguous and come before optional-statements
581 const common_resource_attributes = try self.parseCommonResourceAttributes();
582
583 const button_width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
584
585 try self.nextToken(.normal);
586 try self.check(.comma);
587
588 const button_height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
589
590 try self.nextToken(.normal);
591 const begin_token = self.state.token;
592 try self.check(.begin);
593
594 var buttons = std.ArrayListUnmanaged(*Node){};
595 while (try self.parseToolbarButtonStatement()) |button_node| {
596 try buttons.append(self.state.arena, button_node);
597 }
598
599 try self.nextToken(.normal);
600 const end_token = self.state.token;
601 try self.check(.end);
602
603 const node = try self.state.arena.create(Node.Toolbar);
604 node.* = .{
605 .id = id_token,
606 .type = type_token,
607 .common_resource_attributes = common_resource_attributes,
608 .button_width = button_width,
609 .button_height = button_height,
610 .begin_token = begin_token,
611 .buttons = try buttons.toOwnedSlice(self.state.arena),
612 .end_token = end_token,
613 };
614 return &node.base;
615 },
616 .menu, .menuex => {
617 // common resource attributes must all be contiguous and come before optional-statements
618 const common_resource_attributes = try self.parseCommonResourceAttributes();
619 // help id is optional but must come between common resource attributes and optional-statements
620 var help_id: ?*Node = null;
621 // Note: No comma is allowed before or after help_id of MENUEX and help_id is not
622 // a possible field of MENU.
623 if (resource == .menuex and try self.lookaheadCouldBeNumberExpression(.not_disallowed)) {
624 help_id = try self.parseExpression(.{
625 .is_known_to_be_number_expression = true,
626 });
627 }
628 const optional_statements = try self.parseOptionalStatements(.stringtable);
629
630 try self.nextToken(.normal);
631 const begin_token = self.state.token;
632 try self.check(.begin);
633
634 var items = std.ArrayListUnmanaged(*Node){};
635 defer items.deinit(self.state.allocator);
636 while (try self.parseMenuItemStatement(resource, id_token, 1)) |item_node| {
637 try items.append(self.state.allocator, item_node);
638 }
639
640 try self.nextToken(.normal);
641 const end_token = self.state.token;
642 try self.check(.end);
643
644 if (items.items.len == 0) {
645 return self.addErrorDetailsAndFail(.{
646 .err = .empty_menu_not_allowed,
647 .token = type_token,
648 });
649 }
650
651 const node = try self.state.arena.create(Node.Menu);
652 node.* = .{
653 .id = id_token,
654 .type = type_token,
655 .common_resource_attributes = common_resource_attributes,
656 .optional_statements = optional_statements,
657 .help_id = help_id,
658 .begin_token = begin_token,
659 .items = try self.state.arena.dupe(*Node, items.items),
660 .end_token = end_token,
661 };
662 return &node.base;
663 },
664 .versioninfo => {
665 // common resource attributes must all be contiguous and come before optional-statements
666 const common_resource_attributes = try self.parseCommonResourceAttributes();
667
668 var fixed_info = std.ArrayListUnmanaged(*Node){};
669 while (try self.parseVersionStatement()) |version_statement| {
670 try fixed_info.append(self.state.arena, version_statement);
671 }
672
673 try self.nextToken(.normal);
674 const begin_token = self.state.token;
675 try self.check(.begin);
676
677 var block_statements = std.ArrayListUnmanaged(*Node){};
678 while (try self.parseVersionBlockOrValue(id_token, 1)) |block_node| {
679 try block_statements.append(self.state.arena, block_node);
680 }
681
682 try self.nextToken(.normal);
683 const end_token = self.state.token;
684 try self.check(.end);
685
686 const node = try self.state.arena.create(Node.VersionInfo);
687 node.* = .{
688 .id = id_token,
689 .versioninfo = type_token,
690 .common_resource_attributes = common_resource_attributes,
691 .fixed_info = try fixed_info.toOwnedSlice(self.state.arena),
692 .begin_token = begin_token,
693 .block_statements = try block_statements.toOwnedSlice(self.state.arena),
694 .end_token = end_token,
695 };
696 return &node.base;
697 },
698 .dlginclude => {
699 const common_resource_attributes = try self.parseCommonResourceAttributes();
700
701 var filename_expression = try self.parseExpression(.{
702 .allowed_types = .{ .string = true },
703 });
704
705 const node = try self.state.arena.create(Node.ResourceExternal);
706 node.* = .{
707 .id = id_token,
708 .type = type_token,
709 .common_resource_attributes = common_resource_attributes,
710 .filename = filename_expression,
711 };
712 return &node.base;
713 },
714 .stringtable => {
715 return self.addErrorDetailsAndFail(.{
716 .err = .name_or_id_not_allowed,
717 .token = id_token,
718 .extra = .{ .resource = resource },
719 });
720 },
721 // Just try everything as a 'generic' resource (raw data or external file)
722 // TODO: More fine-grained switch cases as necessary
723 else => {
724 const common_resource_attributes = try self.parseCommonResourceAttributes();
725
726 const maybe_begin = try self.lookaheadToken(.normal);
727 if (maybe_begin.id == .begin) {
728 self.nextToken(.normal) catch unreachable;
729
730 if (!resource.canUseRawData()) {
731 try self.addErrorDetails(ErrorDetails{
732 .err = .resource_type_cant_use_raw_data,
733 .token = maybe_begin,
734 .extra = .{ .resource = resource },
735 });
736 return self.addErrorDetailsAndFail(ErrorDetails{
737 .err = .resource_type_cant_use_raw_data,
738 .type = .note,
739 .print_source_line = false,
740 .token = maybe_begin,
741 });
742 }
743
744 const raw_data = try self.parseRawDataBlock();
745 const end_token = self.state.token;
746
747 const node = try self.state.arena.create(Node.ResourceRawData);
748 node.* = .{
749 .id = id_token,
750 .type = type_token,
751 .common_resource_attributes = common_resource_attributes,
752 .begin_token = maybe_begin,
753 .raw_data = raw_data,
754 .end_token = end_token,
755 };
756 return &node.base;
757 }
758
759 var filename_expression = try self.parseExpression(.{
760 // Don't tell the user that numbers are accepted since we error on
761 // number expressions and regular number literals are treated as unquoted
762 // literals rather than numbers, so from the users perspective
763 // numbers aren't really allowed.
764 .expected_types_override = .{
765 .literal = true,
766 .string_literal = true,
767 },
768 });
769
770 const node = try self.state.arena.create(Node.ResourceExternal);
771 node.* = .{
772 .id = id_token,
773 .type = type_token,
774 .common_resource_attributes = common_resource_attributes,
775 .filename = filename_expression,
776 };
777 return &node.base;
778 },
779 }
780 }
781
782 /// Expects the current token to be a begin token.
783 /// After return, the current token will be the end token.
784 fn parseRawDataBlock(self: *Self) Error![]*Node {
785 var raw_data = std.ArrayList(*Node).init(self.state.allocator);
786 defer raw_data.deinit();
787 while (true) {
788 const maybe_end_token = try self.lookaheadToken(.normal);
789 switch (maybe_end_token.id) {
790 .comma => {
791 // comma as the first token in a raw data block is an error
792 if (raw_data.items.len == 0) {
793 return self.addErrorDetailsAndFail(ErrorDetails{
794 .err = .expected_something_else,
795 .token = maybe_end_token,
796 .extra = .{ .expected_types = .{
797 .number = true,
798 .number_expression = true,
799 .string_literal = true,
800 } },
801 });
802 }
803 // otherwise just skip over commas
804 self.nextToken(.normal) catch unreachable;
805 continue;
806 },
807 .end => {
808 self.nextToken(.normal) catch unreachable;
809 break;
810 },
811 .eof => {
812 return self.addErrorDetailsAndFail(ErrorDetails{
813 .err = .unfinished_raw_data_block,
814 .token = maybe_end_token,
815 });
816 },
817 else => {},
818 }
819 const expression = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
820 try raw_data.append(expression);
821
822 if (expression.isNumberExpression()) {
823 const maybe_close_paren = try self.lookaheadToken(.normal);
824 if (maybe_close_paren.id == .close_paren) {
825 // <number expression>) is an error
826 return self.addErrorDetailsAndFail(ErrorDetails{
827 .err = .expected_token,
828 .token = maybe_close_paren,
829 .extra = .{ .expected = .operator },
830 });
831 }
832 }
833 }
834 return try self.state.arena.dupe(*Node, raw_data.items);
835 }
836
837 /// Expects the current token to be handled, and that the control statement will
838 /// begin on the next token.
839 /// After return, the current token will be the token immediately before the end of the
840 /// control statement (or unchanged if the function returns null).
841 fn parseControlStatement(self: *Self, resource: Resource) Error!?*Node {
842 const control_token = try self.lookaheadToken(.normal);
843 const control = rc.Control.map.get(control_token.slice(self.lexer.buffer)) orelse return null;
844 self.nextToken(.normal) catch unreachable;
845
846 try self.skipAnyCommas();
847
848 var text: ?Token = null;
849 if (control.hasTextParam()) {
850 try self.nextToken(.normal);
851 switch (self.state.token.id) {
852 .quoted_ascii_string, .quoted_wide_string, .number => {
853 text = self.state.token;
854 },
855 else => {
856 return self.addErrorDetailsAndFail(ErrorDetails{
857 .err = .expected_something_else,
858 .token = self.state.token,
859 .extra = .{ .expected_types = .{
860 .number = true,
861 .string_literal = true,
862 } },
863 });
864 },
865 }
866 try self.skipAnyCommas();
867 }
868
869 const id = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
870
871 try self.skipAnyCommas();
872
873 var class: ?*Node = null;
874 var style: ?*Node = null;
875 if (control == .control) {
876 class = try self.parseExpression(.{});
877 if (class.?.id == .literal) {
878 const class_literal = @fieldParentPtr(Node.Literal, "base", class.?);
879 const is_invalid_control_class = class_literal.token.id == .literal and !rc.ControlClass.map.has(class_literal.token.slice(self.lexer.buffer));
880 if (is_invalid_control_class) {
881 return self.addErrorDetailsAndFail(.{
882 .err = .expected_something_else,
883 .token = self.state.token,
884 .extra = .{ .expected_types = .{
885 .control_class = true,
886 } },
887 });
888 }
889 }
890 try self.skipAnyCommas();
891 style = try self.parseExpression(.{
892 .can_contain_not_expressions = true,
893 .allowed_types = .{ .number = true },
894 });
895 // If there is no comma after the style paramter, the Win32 RC compiler
896 // could misinterpret the statement and end up skipping over at least one token
897 // that should have been interepeted as the next parameter (x). For example:
898 // CONTROL "text", 1, BUTTON, 15 30, 1, 2, 3, 4
899 // the `15` is the style parameter, but in the Win32 implementation the `30`
900 // is completely ignored (i.e. the `1, 2, 3, 4` are `x`, `y`, `w`, `h`).
901 // If a comma is added after the `15`, then `30` gets interpreted (correctly)
902 // as the `x` value.
903 //
904 // Instead of emulating this behavior, we just warn about the potential for
905 // weird behavior in the Win32 implementation whenever there isn't a comma after
906 // the style parameter.
907 const lookahead_token = try self.lookaheadToken(.normal);
908 if (lookahead_token.id != .comma and lookahead_token.id != .eof) {
909 try self.addErrorDetails(.{
910 .err = .rc_could_miscompile_control_params,
911 .type = .warning,
912 .token = lookahead_token,
913 });
914 try self.addErrorDetails(.{
915 .err = .rc_could_miscompile_control_params,
916 .type = .note,
917 .token = style.?.getFirstToken(),
918 .token_span_end = style.?.getLastToken(),
919 });
920 }
921 try self.skipAnyCommas();
922 }
923
924 const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
925 _ = try self.parseOptionalToken(.comma);
926 const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
927 _ = try self.parseOptionalToken(.comma);
928 const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
929 _ = try self.parseOptionalToken(.comma);
930 const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
931
932 var optional_param_parser = OptionalParamParser{ .parser = self };
933 if (control != .control) {
934 style = try optional_param_parser.parse(.{ .not_expression_allowed = true });
935 }
936
937 var exstyle: ?*Node = try optional_param_parser.parse(.{ .not_expression_allowed = true });
938 var help_id: ?*Node = switch (resource) {
939 .dialogex => try optional_param_parser.parse(.{}),
940 else => null,
941 };
942
943 var extra_data: []*Node = &[_]*Node{};
944 var extra_data_begin: ?Token = null;
945 var extra_data_end: ?Token = null;
946 // extra data is DIALOGEX-only
947 if (resource == .dialogex and try self.parseOptionalToken(.begin)) {
948 extra_data_begin = self.state.token;
949 extra_data = try self.parseRawDataBlock();
950 extra_data_end = self.state.token;
951 }
952
953 const node = try self.state.arena.create(Node.ControlStatement);
954 node.* = .{
955 .type = control_token,
956 .text = text,
957 .class = class,
958 .id = id,
959 .x = x,
960 .y = y,
961 .width = width,
962 .height = height,
963 .style = style,
964 .exstyle = exstyle,
965 .help_id = help_id,
966 .extra_data_begin = extra_data_begin,
967 .extra_data = extra_data,
968 .extra_data_end = extra_data_end,
969 };
970 return &node.base;
971 }
972
973 fn parseToolbarButtonStatement(self: *Self) Error!?*Node {
974 const keyword_token = try self.lookaheadToken(.normal);
975 const button_type = rc.ToolbarButton.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
976 self.nextToken(.normal) catch unreachable;
977
978 switch (button_type) {
979 .separator => {
980 const node = try self.state.arena.create(Node.Literal);
981 node.* = .{
982 .token = keyword_token,
983 };
984 return &node.base;
985 },
986 .button => {
987 const button_id = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
988
989 const node = try self.state.arena.create(Node.SimpleStatement);
990 node.* = .{
991 .identifier = keyword_token,
992 .value = button_id,
993 };
994 return &node.base;
995 },
996 }
997 }
998
999 /// Expects the current token to be handled, and that the menuitem/popup statement will
1000 /// begin on the next token.
1001 /// After return, the current token will be the token immediately before the end of the
1002 /// menuitem statement (or unchanged if the function returns null).
1003 fn parseMenuItemStatement(self: *Self, resource: Resource, top_level_menu_id_token: Token, nesting_level: u32) Error!?*Node {
1004 const menuitem_token = try self.lookaheadToken(.normal);
1005 const menuitem = rc.MenuItem.map.get(menuitem_token.slice(self.lexer.buffer)) orelse return null;
1006 self.nextToken(.normal) catch unreachable;
1007
1008 if (nesting_level > max_nested_menu_level) {
1009 try self.addErrorDetails(.{
1010 .err = .nested_resource_level_exceeds_max,
1011 .token = top_level_menu_id_token,
1012 .extra = .{ .resource = resource },
1013 });
1014 return self.addErrorDetailsAndFail(.{
1015 .err = .nested_resource_level_exceeds_max,
1016 .type = .note,
1017 .token = menuitem_token,
1018 .extra = .{ .resource = resource },
1019 });
1020 }
1021
1022 switch (resource) {
1023 .menu => switch (menuitem) {
1024 .menuitem => {
1025 try self.nextToken(.normal);
1026 if (rc.MenuItem.isSeparator(self.state.token.slice(self.lexer.buffer))) {
1027 const separator_token = self.state.token;
1028 // There can be any number of trailing commas after SEPARATOR
1029 try self.skipAnyCommas();
1030 const node = try self.state.arena.create(Node.MenuItemSeparator);
1031 node.* = .{
1032 .menuitem = menuitem_token,
1033 .separator = separator_token,
1034 };
1035 return &node.base;
1036 } else {
1037 const text = self.state.token;
1038 if (!text.isStringLiteral()) {
1039 return self.addErrorDetailsAndFail(ErrorDetails{
1040 .err = .expected_something_else,
1041 .token = text,
1042 .extra = .{ .expected_types = .{
1043 .string_literal = true,
1044 } },
1045 });
1046 }
1047 try self.skipAnyCommas();
1048
1049 const result = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1050
1051 _ = try self.parseOptionalToken(.comma);
1052
1053 var options = std.ArrayListUnmanaged(Token){};
1054 while (true) {
1055 const option_token = try self.lookaheadToken(.normal);
1056 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
1057 break;
1058 }
1059 self.nextToken(.normal) catch unreachable;
1060 try options.append(self.state.arena, option_token);
1061 try self.skipAnyCommas();
1062 }
1063
1064 const node = try self.state.arena.create(Node.MenuItem);
1065 node.* = .{
1066 .menuitem = menuitem_token,
1067 .text = text,
1068 .result = result,
1069 .option_list = try options.toOwnedSlice(self.state.arena),
1070 };
1071 return &node.base;
1072 }
1073 },
1074 .popup => {
1075 try self.nextToken(.normal);
1076 const text = self.state.token;
1077 if (!text.isStringLiteral()) {
1078 return self.addErrorDetailsAndFail(ErrorDetails{
1079 .err = .expected_something_else,
1080 .token = text,
1081 .extra = .{ .expected_types = .{
1082 .string_literal = true,
1083 } },
1084 });
1085 }
1086 try self.skipAnyCommas();
1087
1088 var options = std.ArrayListUnmanaged(Token){};
1089 while (true) {
1090 const option_token = try self.lookaheadToken(.normal);
1091 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
1092 break;
1093 }
1094 self.nextToken(.normal) catch unreachable;
1095 try options.append(self.state.arena, option_token);
1096 try self.skipAnyCommas();
1097 }
1098
1099 try self.nextToken(.normal);
1100 const begin_token = self.state.token;
1101 try self.check(.begin);
1102
1103 var items = std.ArrayListUnmanaged(*Node){};
1104 while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| {
1105 try items.append(self.state.arena, item_node);
1106 }
1107
1108 try self.nextToken(.normal);
1109 const end_token = self.state.token;
1110 try self.check(.end);
1111
1112 if (items.items.len == 0) {
1113 return self.addErrorDetailsAndFail(.{
1114 .err = .empty_menu_not_allowed,
1115 .token = menuitem_token,
1116 });
1117 }
1118
1119 const node = try self.state.arena.create(Node.Popup);
1120 node.* = .{
1121 .popup = menuitem_token,
1122 .text = text,
1123 .option_list = try options.toOwnedSlice(self.state.arena),
1124 .begin_token = begin_token,
1125 .items = try items.toOwnedSlice(self.state.arena),
1126 .end_token = end_token,
1127 };
1128 return &node.base;
1129 },
1130 },
1131 .menuex => {
1132 try self.nextToken(.normal);
1133 const text = self.state.token;
1134 if (!text.isStringLiteral()) {
1135 return self.addErrorDetailsAndFail(ErrorDetails{
1136 .err = .expected_something_else,
1137 .token = text,
1138 .extra = .{ .expected_types = .{
1139 .string_literal = true,
1140 } },
1141 });
1142 }
1143
1144 var param_parser = OptionalParamParser{ .parser = self };
1145 const id = try param_parser.parse(.{});
1146 const item_type = try param_parser.parse(.{});
1147 const state = try param_parser.parse(.{});
1148
1149 if (menuitem == .menuitem) {
1150 // trailing comma is allowed, skip it
1151 _ = try self.parseOptionalToken(.comma);
1152
1153 const node = try self.state.arena.create(Node.MenuItemEx);
1154 node.* = .{
1155 .menuitem = menuitem_token,
1156 .text = text,
1157 .id = id,
1158 .type = item_type,
1159 .state = state,
1160 };
1161 return &node.base;
1162 }
1163
1164 const help_id = try param_parser.parse(.{});
1165
1166 // trailing comma is allowed, skip it
1167 _ = try self.parseOptionalToken(.comma);
1168
1169 try self.nextToken(.normal);
1170 const begin_token = self.state.token;
1171 try self.check(.begin);
1172
1173 var items = std.ArrayListUnmanaged(*Node){};
1174 while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| {
1175 try items.append(self.state.arena, item_node);
1176 }
1177
1178 try self.nextToken(.normal);
1179 const end_token = self.state.token;
1180 try self.check(.end);
1181
1182 if (items.items.len == 0) {
1183 return self.addErrorDetailsAndFail(.{
1184 .err = .empty_menu_not_allowed,
1185 .token = menuitem_token,
1186 });
1187 }
1188
1189 const node = try self.state.arena.create(Node.PopupEx);
1190 node.* = .{
1191 .popup = menuitem_token,
1192 .text = text,
1193 .id = id,
1194 .type = item_type,
1195 .state = state,
1196 .help_id = help_id,
1197 .begin_token = begin_token,
1198 .items = try items.toOwnedSlice(self.state.arena),
1199 .end_token = end_token,
1200 };
1201 return &node.base;
1202 },
1203 else => unreachable,
1204 }
1205 @compileError("unreachable");
1206 }
1207
1208 pub const OptionalParamParser = struct {
1209 finished: bool = false,
1210 parser: *Self,
1211
1212 pub const Options = struct {
1213 not_expression_allowed: bool = false,
1214 };
1215
1216 pub fn parse(self: *OptionalParamParser, options: OptionalParamParser.Options) Error!?*Node {
1217 if (self.finished) return null;
1218 if (!(try self.parser.parseOptionalToken(.comma))) {
1219 self.finished = true;
1220 return null;
1221 }
1222 // If the next lookahead token could be part of a number expression,
1223 // then parse it. Otherwise, treat it as an 'empty' expression and
1224 // continue parsing, since 'empty' values are allowed.
1225 if (try self.parser.lookaheadCouldBeNumberExpression(switch (options.not_expression_allowed) {
1226 true => .not_allowed,
1227 false => .not_disallowed,
1228 })) {
1229 const node = try self.parser.parseExpression(.{
1230 .allowed_types = .{ .number = true },
1231 .can_contain_not_expressions = options.not_expression_allowed,
1232 });
1233 return node;
1234 }
1235 return null;
1236 }
1237 };
1238
1239 /// Expects the current token to be handled, and that the version statement will
1240 /// begin on the next token.
1241 /// After return, the current token will be the token immediately before the end of the
1242 /// version statement (or unchanged if the function returns null).
1243 fn parseVersionStatement(self: *Self) Error!?*Node {
1244 const type_token = try self.lookaheadToken(.normal);
1245 const statement_type = rc.VersionInfo.map.get(type_token.slice(self.lexer.buffer)) orelse return null;
1246 self.nextToken(.normal) catch unreachable;
1247 switch (statement_type) {
1248 .file_version, .product_version => {
1249 var parts = std.BoundedArray(*Node, 4){};
1250
1251 while (parts.len < 4) {
1252 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1253 parts.addOneAssumeCapacity().* = value;
1254
1255 if (parts.len == 4 or !(try self.parseOptionalToken(.comma))) {
1256 break;
1257 }
1258 }
1259
1260 const node = try self.state.arena.create(Node.VersionStatement);
1261 node.* = .{
1262 .type = type_token,
1263 .parts = try self.state.arena.dupe(*Node, parts.slice()),
1264 };
1265 return &node.base;
1266 },
1267 else => {
1268 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1269
1270 const node = try self.state.arena.create(Node.SimpleStatement);
1271 node.* = .{
1272 .identifier = type_token,
1273 .value = value,
1274 };
1275 return &node.base;
1276 },
1277 }
1278 }
1279
1280 /// Expects the current token to be handled, and that the version BLOCK/VALUE will
1281 /// begin on the next token.
1282 /// After return, the current token will be the token immediately before the end of the
1283 /// version BLOCK/VALUE (or unchanged if the function returns null).
1284 fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node {
1285 const keyword_token = try self.lookaheadToken(.normal);
1286 const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
1287 self.nextToken(.normal) catch unreachable;
1288
1289 if (nesting_level > max_nested_version_level) {
1290 try self.addErrorDetails(.{
1291 .err = .nested_resource_level_exceeds_max,
1292 .token = top_level_version_id_token,
1293 .extra = .{ .resource = .versioninfo },
1294 });
1295 return self.addErrorDetailsAndFail(.{
1296 .err = .nested_resource_level_exceeds_max,
1297 .type = .note,
1298 .token = keyword_token,
1299 .extra = .{ .resource = .versioninfo },
1300 });
1301 }
1302
1303 try self.nextToken(.normal);
1304 const key = self.state.token;
1305 if (!key.isStringLiteral()) {
1306 return self.addErrorDetailsAndFail(.{
1307 .err = .expected_something_else,
1308 .token = key,
1309 .extra = .{ .expected_types = .{
1310 .string_literal = true,
1311 } },
1312 });
1313 }
1314 // Need to keep track of this to detect a potential miscompilation when
1315 // the comma is omitted and the first value is a quoted string.
1316 const had_comma_before_first_value = try self.parseOptionalToken(.comma);
1317 try self.skipAnyCommas();
1318
1319 const values = try self.parseBlockValuesList(had_comma_before_first_value);
1320
1321 switch (keyword) {
1322 .block => {
1323 try self.nextToken(.normal);
1324 const begin_token = self.state.token;
1325 try self.check(.begin);
1326
1327 var children = std.ArrayListUnmanaged(*Node){};
1328 while (try self.parseVersionBlockOrValue(top_level_version_id_token, nesting_level + 1)) |value_node| {
1329 try children.append(self.state.arena, value_node);
1330 }
1331
1332 try self.nextToken(.normal);
1333 const end_token = self.state.token;
1334 try self.check(.end);
1335
1336 const node = try self.state.arena.create(Node.Block);
1337 node.* = .{
1338 .identifier = keyword_token,
1339 .key = key,
1340 .values = values,
1341 .begin_token = begin_token,
1342 .children = try children.toOwnedSlice(self.state.arena),
1343 .end_token = end_token,
1344 };
1345 return &node.base;
1346 },
1347 .value => {
1348 const node = try self.state.arena.create(Node.BlockValue);
1349 node.* = .{
1350 .identifier = keyword_token,
1351 .key = key,
1352 .values = values,
1353 };
1354 return &node.base;
1355 },
1356 }
1357 }
1358
1359 fn parseBlockValuesList(self: *Self, had_comma_before_first_value: bool) Error![]*Node {
1360 var values = std.ArrayListUnmanaged(*Node){};
1361 var seen_number: bool = false;
1362 var first_string_value: ?*Node = null;
1363 while (true) {
1364 const lookahead_token = try self.lookaheadToken(.normal);
1365 switch (lookahead_token.id) {
1366 .operator,
1367 .number,
1368 .open_paren,
1369 .quoted_ascii_string,
1370 .quoted_wide_string,
1371 => {},
1372 else => break,
1373 }
1374 const value = try self.parseExpression(.{});
1375
1376 if (value.isNumberExpression()) {
1377 seen_number = true;
1378 } else if (first_string_value == null) {
1379 std.debug.assert(value.isStringLiteral());
1380 first_string_value = value;
1381 }
1382
1383 const has_trailing_comma = try self.parseOptionalToken(.comma);
1384 try self.skipAnyCommas();
1385
1386 const value_value = try self.state.arena.create(Node.BlockValueValue);
1387 value_value.* = .{
1388 .expression = value,
1389 .trailing_comma = has_trailing_comma,
1390 };
1391 try values.append(self.state.arena, &value_value.base);
1392 }
1393 if (seen_number and first_string_value != null) {
1394 // The Win32 RC compiler does some strange stuff with the data size:
1395 // Strings are counted as UTF-16 code units including the null-terminator
1396 // Numbers are counted as their byte lengths
1397 // So, when both strings and numbers are within a single value,
1398 // it incorrectly sets the value's type as binary, but then gives the
1399 // data length as a mixture of bytes and UTF-16 code units. This means that
1400 // when the length is read, it will be treated as byte length and will
1401 // not read the full value. We don't reproduce this behavior, so we warn
1402 // of the miscompilation here.
1403 try self.addErrorDetails(.{
1404 .err = .rc_would_miscompile_version_value_byte_count,
1405 .type = .warning,
1406 .token = first_string_value.?.getFirstToken(),
1407 .token_span_start = values.items[0].getFirstToken(),
1408 .token_span_end = values.items[values.items.len - 1].getLastToken(),
1409 });
1410 try self.addErrorDetails(.{
1411 .err = .rc_would_miscompile_version_value_byte_count,
1412 .type = .note,
1413 .token = first_string_value.?.getFirstToken(),
1414 .token_span_start = values.items[0].getFirstToken(),
1415 .token_span_end = values.items[values.items.len - 1].getLastToken(),
1416 .print_source_line = false,
1417 });
1418 }
1419 if (!had_comma_before_first_value and values.items.len > 0 and values.items[0].cast(.block_value_value).?.expression.isStringLiteral()) {
1420 const token = values.items[0].cast(.block_value_value).?.expression.cast(.literal).?.token;
1421 try self.addErrorDetails(.{
1422 .err = .rc_would_miscompile_version_value_padding,
1423 .type = .warning,
1424 .token = token,
1425 });
1426 try self.addErrorDetails(.{
1427 .err = .rc_would_miscompile_version_value_padding,
1428 .type = .note,
1429 .token = token,
1430 .print_source_line = false,
1431 });
1432 }
1433 return values.toOwnedSlice(self.state.arena);
1434 }
1435
1436 fn numberExpressionContainsAnyLSuffixes(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) bool {
1437 // TODO: This could probably be done without evaluating the whole expression
1438 return Compiler.evaluateNumberExpression(expression_node, source, code_page_lookup).is_long;
1439 }
1440
1441 /// Expects the current token to be a literal token that contains the string LANGUAGE
1442 fn parseLanguageStatement(self: *Self) Error!*Node {
1443 const language_token = self.state.token;
1444
1445 const primary_language = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1446
1447 try self.nextToken(.normal);
1448 try self.check(.comma);
1449
1450 const sublanguage = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1451
1452 // The Win32 RC compiler errors if either parameter contains any number with an L
1453 // suffix. Instead of that, we want to warn and then let the values get truncated.
1454 // The warning is done here to allow the compiler logic to not have to deal with this.
1455 if (numberExpressionContainsAnyLSuffixes(primary_language, self.lexer.buffer, &self.state.input_code_page_lookup)) {
1456 try self.addErrorDetails(.{
1457 .err = .rc_would_error_u16_with_l_suffix,
1458 .type = .warning,
1459 .token = primary_language.getFirstToken(),
1460 .token_span_end = primary_language.getLastToken(),
1461 .extra = .{ .statement_with_u16_param = .language },
1462 });
1463 try self.addErrorDetails(.{
1464 .err = .rc_would_error_u16_with_l_suffix,
1465 .print_source_line = false,
1466 .type = .note,
1467 .token = primary_language.getFirstToken(),
1468 .token_span_end = primary_language.getLastToken(),
1469 .extra = .{ .statement_with_u16_param = .language },
1470 });
1471 }
1472 if (numberExpressionContainsAnyLSuffixes(sublanguage, self.lexer.buffer, &self.state.input_code_page_lookup)) {
1473 try self.addErrorDetails(.{
1474 .err = .rc_would_error_u16_with_l_suffix,
1475 .type = .warning,
1476 .token = sublanguage.getFirstToken(),
1477 .token_span_end = sublanguage.getLastToken(),
1478 .extra = .{ .statement_with_u16_param = .language },
1479 });
1480 try self.addErrorDetails(.{
1481 .err = .rc_would_error_u16_with_l_suffix,
1482 .print_source_line = false,
1483 .type = .note,
1484 .token = sublanguage.getFirstToken(),
1485 .token_span_end = sublanguage.getLastToken(),
1486 .extra = .{ .statement_with_u16_param = .language },
1487 });
1488 }
1489
1490 const node = try self.state.arena.create(Node.LanguageStatement);
1491 node.* = .{
1492 .language_token = language_token,
1493 .primary_language_id = primary_language,
1494 .sublanguage_id = sublanguage,
1495 };
1496 return &node.base;
1497 }
1498
1499 pub const ParseExpressionOptions = struct {
1500 is_known_to_be_number_expression: bool = false,
1501 can_contain_not_expressions: bool = false,
1502 nesting_context: NestingContext = .{},
1503 allowed_types: AllowedTypes = .{ .literal = true, .number = true, .string = true },
1504 expected_types_override: ?ErrorDetails.ExpectedTypes = null,
1505
1506 pub const AllowedTypes = struct {
1507 literal: bool = false,
1508 number: bool = false,
1509 string: bool = false,
1510 };
1511
1512 pub const NestingContext = struct {
1513 first_token: ?Token = null,
1514 last_token: ?Token = null,
1515 level: u32 = 0,
1516
1517 /// Returns a new NestingContext with values modified appropriately for an increased nesting level
1518 fn incremented(ctx: NestingContext, first_token: Token, most_recent_token: Token) NestingContext {
1519 return .{
1520 .first_token = ctx.first_token orelse first_token,
1521 .last_token = most_recent_token,
1522 .level = ctx.level + 1,
1523 };
1524 }
1525 };
1526
1527 pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetails {
1528 // TODO: expected_types_override interaction with is_known_to_be_number_expression?
1529 var expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{
1530 .number = options.allowed_types.number,
1531 .number_expression = options.allowed_types.number,
1532 .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression,
1533 .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression,
1534 };
1535 return ErrorDetails{
1536 .err = .expected_something_else,
1537 .token = token,
1538 .extra = .{ .expected_types = expected_types },
1539 };
1540 }
1541 };
1542
1543 /// Returns true if the next lookahead token is a number or could be the start of a number expression.
1544 /// Only useful when looking for empty expressions in optional fields.
1545 fn lookaheadCouldBeNumberExpression(self: *Self, not_allowed: enum { not_allowed, not_disallowed }) Error!bool {
1546 var lookahead_token = try self.lookaheadToken(.normal);
1547 switch (lookahead_token.id) {
1548 .literal => if (not_allowed == .not_allowed) {
1549 return std.ascii.eqlIgnoreCase("NOT", lookahead_token.slice(self.lexer.buffer));
1550 } else return false,
1551 .number => return true,
1552 .open_paren => return true,
1553 .operator => {
1554 // + can be a unary operator, see parseExpression's handling of unary +
1555 const operator_char = lookahead_token.slice(self.lexer.buffer)[0];
1556 return operator_char == '+';
1557 },
1558 else => return false,
1559 }
1560 }
1561
1562 fn parsePrimary(self: *Self, options: ParseExpressionOptions) Error!*Node {
1563 try self.nextToken(.normal);
1564 const first_token = self.state.token;
1565 var is_close_paren_expression = false;
1566 var is_unary_plus_expression = false;
1567 switch (self.state.token.id) {
1568 .quoted_ascii_string, .quoted_wide_string => {
1569 if (!options.allowed_types.string) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1570 const node = try self.state.arena.create(Node.Literal);
1571 node.* = .{ .token = self.state.token };
1572 return &node.base;
1573 },
1574 .literal => {
1575 if (options.can_contain_not_expressions and std.ascii.eqlIgnoreCase("NOT", self.state.token.slice(self.lexer.buffer))) {
1576 const not_token = self.state.token;
1577 try self.nextToken(.normal);
1578 try self.check(.number);
1579 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1580 const node = try self.state.arena.create(Node.NotExpression);
1581 node.* = .{
1582 .not_token = not_token,
1583 .number_token = self.state.token,
1584 };
1585 return &node.base;
1586 }
1587 if (!options.allowed_types.literal) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1588 const node = try self.state.arena.create(Node.Literal);
1589 node.* = .{ .token = self.state.token };
1590 return &node.base;
1591 },
1592 .number => {
1593 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1594 const node = try self.state.arena.create(Node.Literal);
1595 node.* = .{ .token = self.state.token };
1596 return &node.base;
1597 },
1598 .open_paren => {
1599 const open_paren_token = self.state.token;
1600
1601 const expression = try self.parseExpression(.{
1602 .is_known_to_be_number_expression = true,
1603 .can_contain_not_expressions = options.can_contain_not_expressions,
1604 .nesting_context = options.nesting_context.incremented(first_token, open_paren_token),
1605 .allowed_types = .{ .number = true },
1606 });
1607
1608 try self.nextToken(.normal);
1609 // TODO: Add context to error about where the open paren is
1610 try self.check(.close_paren);
1611
1612 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(open_paren_token));
1613 const node = try self.state.arena.create(Node.GroupedExpression);
1614 node.* = .{
1615 .open_token = open_paren_token,
1616 .expression = expression,
1617 .close_token = self.state.token,
1618 };
1619 return &node.base;
1620 },
1621 .close_paren => {
1622 // Note: In the Win32 implementation, a single close paren
1623 // counts as a valid "expression", but only when its the first and
1624 // only token in the expression. Such an expression is then treated
1625 // as a 'skip this expression' instruction. For example:
1626 // 1 RCDATA { 1, ), ), ), 2 }
1627 // will be evaluated as if it were `1 RCDATA { 1, 2 }` and only
1628 // 0x0001 and 0x0002 will be written to the .res data.
1629 //
1630 // This behavior is not emulated because it almost certainly has
1631 // no valid use cases and only introduces edge cases that are
1632 // not worth the effort to track down and deal with. Instead,
1633 // we error but also add a note about the Win32 RC behavior if
1634 // this edge case is detected.
1635 if (!options.is_known_to_be_number_expression) {
1636 is_close_paren_expression = true;
1637 }
1638 },
1639 .operator => {
1640 // In the Win32 implementation, something akin to a unary +
1641 // is allowed but it doesn't behave exactly like a unary +.
1642 // Instead of emulating the Win32 behavior, we instead error
1643 // and add a note about unary plus not being allowed.
1644 //
1645 // This is done because unary + only works in some places,
1646 // and there's no real use-case for it since it's so limited
1647 // in how it can be used (e.g. +1 is accepted but (+1) will error)
1648 //
1649 // Even understanding when unary plus is allowed is difficult, so
1650 // we don't do any fancy detection of when the Win32 RC compiler would
1651 // allow a unary + and instead just output the note in all cases.
1652 //
1653 // Some examples of allowed expressions by the Win32 compiler:
1654 // +1
1655 // 0|+5
1656 // +1+2
1657 // +~-5
1658 // +(1)
1659 //
1660 // Some examples of disallowed expressions by the Win32 compiler:
1661 // (+1)
1662 // ++5
1663 //
1664 // TODO: Potentially re-evaluate and support the unary plus in a bug-for-bug
1665 // compatible way.
1666 const operator_char = self.state.token.slice(self.lexer.buffer)[0];
1667 if (operator_char == '+') {
1668 is_unary_plus_expression = true;
1669 }
1670 },
1671 else => {},
1672 }
1673
1674 try self.addErrorDetails(options.toErrorDetails(self.state.token));
1675 if (is_close_paren_expression) {
1676 try self.addErrorDetails(ErrorDetails{
1677 .err = .close_paren_expression,
1678 .type = .note,
1679 .token = self.state.token,
1680 .print_source_line = false,
1681 });
1682 }
1683 if (is_unary_plus_expression) {
1684 try self.addErrorDetails(ErrorDetails{
1685 .err = .unary_plus_expression,
1686 .type = .note,
1687 .token = self.state.token,
1688 .print_source_line = false,
1689 });
1690 }
1691 return error.ParseError;
1692 }
1693
1694 /// Expects the current token to have already been dealt with, and that the
1695 /// expression will start on the next token.
1696 /// After return, the current token will have been dealt with.
1697 fn parseExpression(self: *Self, options: ParseExpressionOptions) Error!*Node {
1698 if (options.nesting_context.level > max_nested_expression_level) {
1699 try self.addErrorDetails(.{
1700 .err = .nested_expression_level_exceeds_max,
1701 .token = options.nesting_context.first_token.?,
1702 });
1703 return self.addErrorDetailsAndFail(.{
1704 .err = .nested_expression_level_exceeds_max,
1705 .type = .note,
1706 .token = options.nesting_context.last_token.?,
1707 });
1708 }
1709 var expr: *Node = try self.parsePrimary(options);
1710 const first_token = expr.getFirstToken();
1711
1712 // Non-number expressions can't have operators, so we can just return
1713 if (!expr.isNumberExpression()) return expr;
1714
1715 while (try self.parseOptionalTokenAdvanced(.operator, .normal_expect_operator)) {
1716 const operator = self.state.token;
1717 const rhs_node = try self.parsePrimary(.{
1718 .is_known_to_be_number_expression = true,
1719 .can_contain_not_expressions = options.can_contain_not_expressions,
1720 .nesting_context = options.nesting_context.incremented(first_token, operator),
1721 .allowed_types = options.allowed_types,
1722 });
1723
1724 if (!rhs_node.isNumberExpression()) {
1725 return self.addErrorDetailsAndFail(ErrorDetails{
1726 .err = .expected_something_else,
1727 .token = rhs_node.getFirstToken(),
1728 .token_span_end = rhs_node.getLastToken(),
1729 .extra = .{ .expected_types = .{
1730 .number = true,
1731 .number_expression = true,
1732 } },
1733 });
1734 }
1735
1736 const node = try self.state.arena.create(Node.BinaryExpression);
1737 node.* = .{
1738 .left = expr,
1739 .operator = operator,
1740 .right = rhs_node,
1741 };
1742 expr = &node.base;
1743 }
1744
1745 return expr;
1746 }
1747
1748 /// Skips any amount of commas (including zero)
1749 /// In other words, it will skip the regex `,*`
1750 /// Assumes the token(s) should be parsed with `.normal` as the method.
1751 fn skipAnyCommas(self: *Self) !void {
1752 while (try self.parseOptionalToken(.comma)) {}
1753 }
1754
1755 /// Advances the current token only if the token's id matches the specified `id`.
1756 /// Assumes the token should be parsed with `.normal` as the method.
1757 /// Returns true if the token matched, false otherwise.
1758 fn parseOptionalToken(self: *Self, id: Token.Id) Error!bool {
1759 return self.parseOptionalTokenAdvanced(id, .normal);
1760 }
1761
1762 /// Advances the current token only if the token's id matches the specified `id`.
1763 /// Returns true if the token matched, false otherwise.
1764 fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool {
1765 const maybe_token = try self.lookaheadToken(method);
1766 if (maybe_token.id != id) return false;
1767 self.nextToken(method) catch unreachable;
1768 return true;
1769 }
1770
1771 fn addErrorDetails(self: *Self, details: ErrorDetails) Allocator.Error!void {
1772 try self.state.diagnostics.append(details);
1773 }
1774
1775 fn addErrorDetailsAndFail(self: *Self, details: ErrorDetails) Error {
1776 try self.addErrorDetails(details);
1777 return error.ParseError;
1778 }
1779
1780 fn nextToken(self: *Self, comptime method: Lexer.LexMethod) Error!void {
1781 self.state.token = token: while (true) {
1782 const token = self.lexer.next(method) catch |err| switch (err) {
1783 error.CodePagePragmaInIncludedFile => {
1784 // The Win32 RC compiler silently ignores such `#pragma code_point` directives,
1785 // but we want to both ignore them *and* emit a warning
1786 try self.addErrorDetails(.{
1787 .err = .code_page_pragma_in_included_file,
1788 .type = .warning,
1789 .token = self.lexer.error_context_token.?,
1790 });
1791 continue;
1792 },
1793 error.CodePagePragmaInvalidCodePage => {
1794 var details = self.lexer.getErrorDetails(err);
1795 if (!self.options.warn_instead_of_error_on_invalid_code_page) {
1796 return self.addErrorDetailsAndFail(details);
1797 }
1798 details.type = .warning;
1799 try self.addErrorDetails(details);
1800 continue;
1801 },
1802 error.InvalidDigitCharacterInNumberLiteral => {
1803 const details = self.lexer.getErrorDetails(err);
1804 try self.addErrorDetails(details);
1805 return self.addErrorDetailsAndFail(.{
1806 .err = details.err,
1807 .type = .note,
1808 .token = details.token,
1809 .print_source_line = false,
1810 });
1811 },
1812 else => return self.addErrorDetailsAndFail(self.lexer.getErrorDetails(err)),
1813 };
1814 break :token token;
1815 };
1816 // After every token, set the input code page for its line
1817 try self.state.input_code_page_lookup.setForToken(self.state.token, self.lexer.current_code_page);
1818 // But only set the output code page to the current code page if we are past the first code_page pragma in the file.
1819 // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that
1820 // don't have an explicit output code page set.
1821 const output_code_page = if (self.lexer.seen_pragma_code_pages > 1) self.lexer.current_code_page else self.state.output_code_page_lookup.default_code_page;
1822 try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page);
1823 }
1824
1825 fn lookaheadToken(self: *Self, comptime method: Lexer.LexMethod) Error!Token {
1826 self.state.lookahead_lexer = self.lexer.*;
1827 return token: while (true) {
1828 break :token self.state.lookahead_lexer.next(method) catch |err| switch (err) {
1829 // Ignore this error and get the next valid token, we'll deal with this
1830 // properly when getting the token for real
1831 error.CodePagePragmaInIncludedFile => continue,
1832 else => return self.addErrorDetailsAndFail(self.state.lookahead_lexer.getErrorDetails(err)),
1833 };
1834 };
1835 }
1836
1837 fn tokenSlice(self: *Self) []const u8 {
1838 return self.state.token.slice(self.lexer.buffer);
1839 }
1840
1841 /// Check that the current token is something that can be used as an ID
1842 fn checkId(self: *Self) !void {
1843 switch (self.state.token.id) {
1844 .literal => {},
1845 else => {
1846 return self.addErrorDetailsAndFail(ErrorDetails{
1847 .err = .expected_token,
1848 .token = self.state.token,
1849 .extra = .{ .expected = .literal },
1850 });
1851 },
1852 }
1853 }
1854
1855 fn check(self: *Self, expected_token_id: Token.Id) !void {
1856 if (self.state.token.id != expected_token_id) {
1857 return self.addErrorDetailsAndFail(ErrorDetails{
1858 .err = .expected_token,
1859 .token = self.state.token,
1860 .extra = .{ .expected = expected_token_id },
1861 });
1862 }
1863 }
1864
1865 fn checkResource(self: *Self) !Resource {
1866 switch (self.state.token.id) {
1867 .literal => return Resource.fromString(.{
1868 .slice = self.state.token.slice(self.lexer.buffer),
1869 .code_page = self.lexer.current_code_page,
1870 }),
1871 else => {
1872 return self.addErrorDetailsAndFail(ErrorDetails{
1873 .err = .expected_token,
1874 .token = self.state.token,
1875 .extra = .{ .expected = .literal },
1876 });
1877 },
1878 }
1879 }
1880};
src/resinator/rc.zig created+407
......@@ -0,0 +1,407 @@
1const std = @import("std");
2const utils = @import("utils.zig");
3const res = @import("res.zig");
4const SourceBytes = @import("literals.zig").SourceBytes;
5
6// https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files
7
8pub const Resource = enum {
9 accelerators,
10 bitmap,
11 cursor,
12 dialog,
13 dialogex,
14 /// As far as I can tell, this is undocumented; the most I could find was this:
15 /// https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/91697
16 dlginclude,
17 /// Undocumented, basically works exactly like RCDATA
18 dlginit,
19 font,
20 html,
21 icon,
22 menu,
23 menuex,
24 messagetable,
25 plugplay, // Obsolete
26 rcdata,
27 stringtable,
28 /// Undocumented
29 toolbar,
30 user_defined,
31 versioninfo,
32 vxd, // Obsolete
33
34 // Types that are treated as a user-defined type when encountered, but have
35 // special meaning without the Visual Studio GUI. We match the Win32 RC compiler
36 // behavior by acting as if these keyword don't exist when compiling the .rc
37 // (thereby treating them as user-defined).
38 //textinclude, // A special resource that is interpreted by Visual C++.
39 //typelib, // A special resource that is used with the /TLBID and /TLBOUT linker options
40
41 // Types that can only be specified by numbers, they don't have keywords
42 cursor_num,
43 icon_num,
44 string_num,
45 anicursor_num,
46 aniicon_num,
47 fontdir_num,
48 manifest_num,
49
50 const map = std.ComptimeStringMapWithEql(Resource, .{
51 .{ "ACCELERATORS", .accelerators },
52 .{ "BITMAP", .bitmap },
53 .{ "CURSOR", .cursor },
54 .{ "DIALOG", .dialog },
55 .{ "DIALOGEX", .dialogex },
56 .{ "DLGINCLUDE", .dlginclude },
57 .{ "DLGINIT", .dlginit },
58 .{ "FONT", .font },
59 .{ "HTML", .html },
60 .{ "ICON", .icon },
61 .{ "MENU", .menu },
62 .{ "MENUEX", .menuex },
63 .{ "MESSAGETABLE", .messagetable },
64 .{ "PLUGPLAY", .plugplay },
65 .{ "RCDATA", .rcdata },
66 .{ "STRINGTABLE", .stringtable },
67 .{ "TOOLBAR", .toolbar },
68 .{ "VERSIONINFO", .versioninfo },
69 .{ "VXD", .vxd },
70 }, std.comptime_string_map.eqlAsciiIgnoreCase);
71
72 pub fn fromString(bytes: SourceBytes) Resource {
73 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes);
74 if (maybe_ordinal) |ordinal| {
75 if (ordinal.ordinal >= 256) return .user_defined;
76 return fromRT(@enumFromInt(ordinal.ordinal));
77 }
78 return map.get(bytes.slice) orelse .user_defined;
79 }
80
81 // TODO: Some comptime validation that RT <-> Resource conversion is synced?
82 pub fn fromRT(rt: res.RT) Resource {
83 return switch (rt) {
84 .ACCELERATOR => .accelerators,
85 .ANICURSOR => .anicursor_num,
86 .ANIICON => .aniicon_num,
87 .BITMAP => .bitmap,
88 .CURSOR => .cursor_num,
89 .DIALOG => .dialog,
90 .DLGINCLUDE => .dlginclude,
91 .DLGINIT => .dlginit,
92 .FONT => .font,
93 .FONTDIR => .fontdir_num,
94 .GROUP_CURSOR => .cursor,
95 .GROUP_ICON => .icon,
96 .HTML => .html,
97 .ICON => .icon_num,
98 .MANIFEST => .manifest_num,
99 .MENU => .menu,
100 .MESSAGETABLE => .messagetable,
101 .PLUGPLAY => .plugplay,
102 .RCDATA => .rcdata,
103 .STRING => .string_num,
104 .TOOLBAR => .toolbar,
105 .VERSION => .versioninfo,
106 .VXD => .vxd,
107 _ => .user_defined,
108 };
109 }
110
111 pub fn canUseRawData(resource: Resource) bool {
112 return switch (resource) {
113 .user_defined,
114 .html,
115 .plugplay, // Obsolete
116 .rcdata,
117 .vxd, // Obsolete
118 .manifest_num,
119 .dlginit,
120 => true,
121 else => false,
122 };
123 }
124
125 pub fn nameForErrorDisplay(resource: Resource) []const u8 {
126 return switch (resource) {
127 // zig fmt: off
128 .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font,
129 .html, .icon, .menu, .menuex, .messagetable, .plugplay, .rcdata, .stringtable,
130 .toolbar, .versioninfo, .vxd => @tagName(resource),
131 // zig fmt: on
132 .user_defined => "user-defined",
133 .cursor_num => std.fmt.comptimePrint("{d} (cursor)", .{@intFromEnum(res.RT.CURSOR)}),
134 .icon_num => std.fmt.comptimePrint("{d} (icon)", .{@intFromEnum(res.RT.ICON)}),
135 .string_num => std.fmt.comptimePrint("{d} (string)", .{@intFromEnum(res.RT.STRING)}),
136 .anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@intFromEnum(res.RT.ANICURSOR)}),
137 .aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@intFromEnum(res.RT.ANIICON)}),
138 .fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@intFromEnum(res.RT.FONTDIR)}),
139 .manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@intFromEnum(res.RT.MANIFEST)}),
140 };
141 }
142};
143
144/// https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable-resource#parameters
145/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialog-resource#parameters
146/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialogex-resource#parameters
147pub const OptionalStatements = enum {
148 characteristics,
149 language,
150 version,
151
152 // DIALOG
153 caption,
154 class,
155 exstyle,
156 font,
157 menu,
158 style,
159
160 pub const map = std.ComptimeStringMapWithEql(OptionalStatements, .{
161 .{ "CHARACTERISTICS", .characteristics },
162 .{ "LANGUAGE", .language },
163 .{ "VERSION", .version },
164 }, std.comptime_string_map.eqlAsciiIgnoreCase);
165
166 pub const dialog_map = std.ComptimeStringMapWithEql(OptionalStatements, .{
167 .{ "CAPTION", .caption },
168 .{ "CLASS", .class },
169 .{ "EXSTYLE", .exstyle },
170 .{ "FONT", .font },
171 .{ "MENU", .menu },
172 .{ "STYLE", .style },
173 }, std.comptime_string_map.eqlAsciiIgnoreCase);
174};
175
176pub const Control = enum {
177 auto3state,
178 autocheckbox,
179 autoradiobutton,
180 checkbox,
181 combobox,
182 control,
183 ctext,
184 defpushbutton,
185 edittext,
186 hedit,
187 iedit,
188 groupbox,
189 icon,
190 listbox,
191 ltext,
192 pushbox,
193 pushbutton,
194 radiobutton,
195 rtext,
196 scrollbar,
197 state3,
198 userbutton,
199
200 pub const map = std.ComptimeStringMapWithEql(Control, .{
201 .{ "AUTO3STATE", .auto3state },
202 .{ "AUTOCHECKBOX", .autocheckbox },
203 .{ "AUTORADIOBUTTON", .autoradiobutton },
204 .{ "CHECKBOX", .checkbox },
205 .{ "COMBOBOX", .combobox },
206 .{ "CONTROL", .control },
207 .{ "CTEXT", .ctext },
208 .{ "DEFPUSHBUTTON", .defpushbutton },
209 .{ "EDITTEXT", .edittext },
210 .{ "HEDIT", .hedit },
211 .{ "IEDIT", .iedit },
212 .{ "GROUPBOX", .groupbox },
213 .{ "ICON", .icon },
214 .{ "LISTBOX", .listbox },
215 .{ "LTEXT", .ltext },
216 .{ "PUSHBOX", .pushbox },
217 .{ "PUSHBUTTON", .pushbutton },
218 .{ "RADIOBUTTON", .radiobutton },
219 .{ "RTEXT", .rtext },
220 .{ "SCROLLBAR", .scrollbar },
221 .{ "STATE3", .state3 },
222 .{ "USERBUTTON", .userbutton },
223 }, std.comptime_string_map.eqlAsciiIgnoreCase);
224
225 pub fn hasTextParam(control: Control) bool {
226 switch (control) {
227 .scrollbar, .listbox, .iedit, .hedit, .edittext, .combobox => return false,
228 else => return true,
229 }
230 }
231};
232
233pub const ControlClass = struct {
234 pub const map = std.ComptimeStringMapWithEql(res.ControlClass, .{
235 .{ "BUTTON", .button },
236 .{ "EDIT", .edit },
237 .{ "STATIC", .static },
238 .{ "LISTBOX", .listbox },
239 .{ "SCROLLBAR", .scrollbar },
240 .{ "COMBOBOX", .combobox },
241 }, std.comptime_string_map.eqlAsciiIgnoreCase);
242
243 /// Like `map.get` but works on WTF16 strings, for use with parsed
244 /// string literals ("BUTTON", or even "\x42UTTON")
245 pub fn fromWideString(str: []const u16) ?res.ControlClass {
246 const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
247 return if (ascii.eqlIgnoreCaseW(str, utf16Literal("BUTTON")))
248 .button
249 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("EDIT")))
250 .edit
251 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("STATIC")))
252 .static
253 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("LISTBOX")))
254 .listbox
255 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("SCROLLBAR")))
256 .scrollbar
257 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("COMBOBOX")))
258 .combobox
259 else
260 null;
261 }
262};
263
264const ascii = struct {
265 /// Compares ASCII values case-insensitively, non-ASCII values are compared directly
266 pub fn eqlIgnoreCaseW(a: []const u16, b: []const u16) bool {
267 if (a.len != b.len) return false;
268 for (a, b) |a_c, b_c| {
269 if (a_c < 128) {
270 if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false;
271 } else {
272 if (a_c != b_c) return false;
273 }
274 }
275 return true;
276 }
277};
278
279pub const MenuItem = enum {
280 menuitem,
281 popup,
282
283 pub const map = std.ComptimeStringMapWithEql(MenuItem, .{
284 .{ "MENUITEM", .menuitem },
285 .{ "POPUP", .popup },
286 }, std.comptime_string_map.eqlAsciiIgnoreCase);
287
288 pub fn isSeparator(bytes: []const u8) bool {
289 return std.ascii.eqlIgnoreCase(bytes, "SEPARATOR");
290 }
291
292 pub const Option = enum {
293 checked,
294 grayed,
295 help,
296 inactive,
297 menubarbreak,
298 menubreak,
299
300 pub const map = std.ComptimeStringMapWithEql(Option, .{
301 .{ "CHECKED", .checked },
302 .{ "GRAYED", .grayed },
303 .{ "HELP", .help },
304 .{ "INACTIVE", .inactive },
305 .{ "MENUBARBREAK", .menubarbreak },
306 .{ "MENUBREAK", .menubreak },
307 }, std.comptime_string_map.eqlAsciiIgnoreCase);
308 };
309};
310
311pub const ToolbarButton = enum {
312 button,
313 separator,
314
315 pub const map = std.ComptimeStringMapWithEql(ToolbarButton, .{
316 .{ "BUTTON", .button },
317 .{ "SEPARATOR", .separator },
318 }, std.comptime_string_map.eqlAsciiIgnoreCase);
319};
320
321pub const VersionInfo = enum {
322 file_version,
323 product_version,
324 file_flags_mask,
325 file_flags,
326 file_os,
327 file_type,
328 file_subtype,
329
330 pub const map = std.ComptimeStringMapWithEql(VersionInfo, .{
331 .{ "FILEVERSION", .file_version },
332 .{ "PRODUCTVERSION", .product_version },
333 .{ "FILEFLAGSMASK", .file_flags_mask },
334 .{ "FILEFLAGS", .file_flags },
335 .{ "FILEOS", .file_os },
336 .{ "FILETYPE", .file_type },
337 .{ "FILESUBTYPE", .file_subtype },
338 }, std.comptime_string_map.eqlAsciiIgnoreCase);
339};
340
341pub const VersionBlock = enum {
342 block,
343 value,
344
345 pub const map = std.ComptimeStringMapWithEql(VersionBlock, .{
346 .{ "BLOCK", .block },
347 .{ "VALUE", .value },
348 }, std.comptime_string_map.eqlAsciiIgnoreCase);
349};
350
351/// Keywords that are be the first token in a statement and (if so) dictate how the rest
352/// of the statement is parsed.
353pub const TopLevelKeywords = enum {
354 language,
355 version,
356 characteristics,
357 stringtable,
358
359 pub const map = std.ComptimeStringMapWithEql(TopLevelKeywords, .{
360 .{ "LANGUAGE", .language },
361 .{ "VERSION", .version },
362 .{ "CHARACTERISTICS", .characteristics },
363 .{ "STRINGTABLE", .stringtable },
364 }, std.comptime_string_map.eqlAsciiIgnoreCase);
365};
366
367pub const CommonResourceAttributes = enum {
368 preload,
369 loadoncall,
370 fixed,
371 moveable,
372 discardable,
373 pure,
374 impure,
375 shared,
376 nonshared,
377
378 pub const map = std.ComptimeStringMapWithEql(CommonResourceAttributes, .{
379 .{ "PRELOAD", .preload },
380 .{ "LOADONCALL", .loadoncall },
381 .{ "FIXED", .fixed },
382 .{ "MOVEABLE", .moveable },
383 .{ "DISCARDABLE", .discardable },
384 .{ "PURE", .pure },
385 .{ "IMPURE", .impure },
386 .{ "SHARED", .shared },
387 .{ "NONSHARED", .nonshared },
388 }, std.comptime_string_map.eqlAsciiIgnoreCase);
389};
390
391pub const AcceleratorTypeAndOptions = enum {
392 virtkey,
393 ascii,
394 noinvert,
395 alt,
396 shift,
397 control,
398
399 pub const map = std.ComptimeStringMapWithEql(AcceleratorTypeAndOptions, .{
400 .{ "VIRTKEY", .virtkey },
401 .{ "ASCII", .ascii },
402 .{ "NOINVERT", .noinvert },
403 .{ "ALT", .alt },
404 .{ "SHIFT", .shift },
405 .{ "CONTROL", .control },
406 }, std.comptime_string_map.eqlAsciiIgnoreCase);
407};
src/resinator/res.zig created+1108
......@@ -0,0 +1,1108 @@
1const std = @import("std");
2const rc = @import("rc.zig");
3const Resource = rc.Resource;
4const CommonResourceAttributes = rc.CommonResourceAttributes;
5const Allocator = std.mem.Allocator;
6const windows1252 = @import("windows1252.zig");
7const CodePage = @import("code_pages.zig").CodePage;
8const literals = @import("literals.zig");
9const SourceBytes = literals.SourceBytes;
10const Codepoint = @import("code_pages.zig").Codepoint;
11const lang = @import("lang.zig");
12const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
13
14/// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
15pub const RT = enum(u8) {
16 ACCELERATOR = 9,
17 ANICURSOR = 21,
18 ANIICON = 22,
19 BITMAP = 2,
20 CURSOR = 1,
21 DIALOG = 5,
22 DLGINCLUDE = 17,
23 DLGINIT = 240,
24 FONT = 8,
25 FONTDIR = 7,
26 GROUP_CURSOR = 1 + 11, // CURSOR + 11
27 GROUP_ICON = 3 + 11, // ICON + 11
28 HTML = 23,
29 ICON = 3,
30 MANIFEST = 24,
31 MENU = 4,
32 MESSAGETABLE = 11,
33 PLUGPLAY = 19,
34 RCDATA = 10,
35 STRING = 6,
36 TOOLBAR = 241,
37 VERSION = 16,
38 VXD = 20,
39 _,
40
41 /// Returns null if the resource type is user-defined
42 /// Asserts that the resource is not `stringtable`
43 pub fn fromResource(resource: Resource) ?RT {
44 return switch (resource) {
45 .accelerators => .ACCELERATOR,
46 .bitmap => .BITMAP,
47 .cursor => .GROUP_CURSOR,
48 .dialog => .DIALOG,
49 .dialogex => .DIALOG,
50 .dlginclude => .DLGINCLUDE,
51 .dlginit => .DLGINIT,
52 .font => .FONT,
53 .html => .HTML,
54 .icon => .GROUP_ICON,
55 .menu => .MENU,
56 .menuex => .MENU,
57 .messagetable => .MESSAGETABLE,
58 .plugplay => .PLUGPLAY,
59 .rcdata => .RCDATA,
60 .stringtable => unreachable,
61 .toolbar => .TOOLBAR,
62 .user_defined => null,
63 .versioninfo => .VERSION,
64 .vxd => .VXD,
65
66 .cursor_num => .CURSOR,
67 .icon_num => .ICON,
68 .string_num => .STRING,
69 .anicursor_num => .ANICURSOR,
70 .aniicon_num => .ANIICON,
71 .fontdir_num => .FONTDIR,
72 .manifest_num => .MANIFEST,
73 };
74 }
75};
76
77/// https://learn.microsoft.com/en-us/windows/win32/menurc/common-resource-attributes
78/// https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader
79pub const MemoryFlags = packed struct(u16) {
80 value: u16,
81
82 pub const MOVEABLE: u16 = 0x10;
83 // TODO: SHARED and PURE seem to be the same thing? Testing seems to confirm this but
84 // would like to find mention of it somewhere.
85 pub const SHARED: u16 = 0x20;
86 pub const PURE: u16 = 0x20;
87 pub const PRELOAD: u16 = 0x40;
88 pub const DISCARDABLE: u16 = 0x1000;
89
90 /// Note: The defaults can have combinations that are not possible to specify within
91 /// an .rc file, as the .rc attributes imply other values (i.e. specifying
92 /// DISCARDABLE always implies MOVEABLE and PURE/SHARED, and yet RT_ICON
93 /// has a default of only MOVEABLE | DISCARDABLE).
94 pub fn defaults(predefined_resource_type: ?RT) MemoryFlags {
95 if (predefined_resource_type == null) {
96 return MemoryFlags{ .value = MOVEABLE | SHARED };
97 } else {
98 return switch (predefined_resource_type.?) {
99 // zig fmt: off
100 .RCDATA, .BITMAP, .HTML, .MANIFEST,
101 .ACCELERATOR, .VERSION, .MESSAGETABLE,
102 .DLGINIT, .TOOLBAR, .PLUGPLAY,
103 .VXD, => MemoryFlags{ .value = MOVEABLE | SHARED },
104
105 .GROUP_ICON, .GROUP_CURSOR,
106 .STRING, .FONT, .DIALOG, .MENU,
107 .DLGINCLUDE, => MemoryFlags{ .value = MOVEABLE | SHARED | DISCARDABLE },
108
109 .ICON, .CURSOR, .ANIICON, .ANICURSOR => MemoryFlags{ .value = MOVEABLE | DISCARDABLE },
110 .FONTDIR => MemoryFlags{ .value = MOVEABLE | PRELOAD },
111 // zig fmt: on
112 // Same as predefined_resource_type == null
113 _ => return MemoryFlags{ .value = MOVEABLE | SHARED },
114 };
115 }
116 }
117
118 pub fn set(self: *MemoryFlags, attribute: CommonResourceAttributes) void {
119 switch (attribute) {
120 .preload => self.value |= PRELOAD,
121 .loadoncall => self.value &= ~PRELOAD,
122 .moveable => self.value |= MOVEABLE,
123 .fixed => self.value &= ~(MOVEABLE | DISCARDABLE),
124 .shared => self.value |= SHARED,
125 .nonshared => self.value &= ~(SHARED | DISCARDABLE),
126 .pure => self.value |= PURE,
127 .impure => self.value &= ~(PURE | DISCARDABLE),
128 .discardable => self.value |= DISCARDABLE | MOVEABLE | PURE,
129 }
130 }
131
132 pub fn setGroup(self: *MemoryFlags, attribute: CommonResourceAttributes, implied_shared_or_pure: bool) void {
133 switch (attribute) {
134 .preload => {
135 self.value |= PRELOAD;
136 if (implied_shared_or_pure) self.value &= ~SHARED;
137 },
138 .loadoncall => {
139 self.value &= ~PRELOAD;
140 if (implied_shared_or_pure) self.value |= SHARED;
141 },
142 else => self.set(attribute),
143 }
144 }
145};
146
147/// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers
148pub const Language = packed struct(u16) {
149 // Note: This is the default no matter what locale the current system is set to,
150 // e.g. even if the system's locale is en-GB, en-US will still be the
151 // default language for resources in the Win32 rc compiler.
152 primary_language_id: u10 = lang.LANG_ENGLISH,
153 sublanguage_id: u6 = lang.SUBLANG_ENGLISH_US,
154
155 /// Default language ID as a u16
156 pub const default: u16 = (Language{}).asInt();
157
158 pub fn fromInt(int: u16) Language {
159 return @bitCast(int);
160 }
161
162 pub fn asInt(self: Language) u16 {
163 return @bitCast(self);
164 }
165};
166
167/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks
168pub const ControlClass = enum(u16) {
169 button = 0x80,
170 edit = 0x81,
171 static = 0x82,
172 listbox = 0x83,
173 scrollbar = 0x84,
174 combobox = 0x85,
175
176 pub fn fromControl(control: rc.Control) ?ControlClass {
177 return switch (control) {
178 // zig fmt: off
179 .auto3state, .autocheckbox, .autoradiobutton,
180 .checkbox, .defpushbutton, .groupbox, .pushbox,
181 .pushbutton, .radiobutton, .state3, .userbutton => .button,
182 // zig fmt: on
183 .combobox => .combobox,
184 .control => null,
185 .ctext, .icon, .ltext, .rtext => .static,
186 .edittext, .hedit, .iedit => .edit,
187 .listbox => .listbox,
188 .scrollbar => .scrollbar,
189 };
190 }
191
192 pub fn getImpliedStyle(control: rc.Control) u32 {
193 var style = WS.CHILD | WS.VISIBLE;
194 switch (control) {
195 .auto3state => style |= BS.AUTO3STATE | WS.TABSTOP,
196 .autocheckbox => style |= BS.AUTOCHECKBOX | WS.TABSTOP,
197 .autoradiobutton => style |= BS.AUTORADIOBUTTON,
198 .checkbox => style |= BS.CHECKBOX | WS.TABSTOP,
199 .combobox => {},
200 .control => {},
201 .ctext => style |= SS.CENTER | WS.GROUP,
202 .defpushbutton => style |= BS.DEFPUSHBUTTON | WS.TABSTOP,
203 .edittext, .hedit, .iedit => style |= WS.TABSTOP | WS.BORDER,
204 .groupbox => style |= BS.GROUPBOX,
205 .icon => style |= SS.ICON,
206 .listbox => style |= LBS.NOTIFY | WS.BORDER,
207 .ltext => style |= WS.GROUP,
208 .pushbox => style |= BS.PUSHBOX | WS.TABSTOP,
209 .pushbutton => style |= WS.TABSTOP,
210 .radiobutton => style |= BS.RADIOBUTTON,
211 .rtext => style |= SS.RIGHT | WS.GROUP,
212 .scrollbar => {},
213 .state3 => style |= BS.@"3STATE" | WS.TABSTOP,
214 .userbutton => style |= BS.USERBUTTON | WS.TABSTOP,
215 }
216 return style;
217 }
218};
219
220pub const NameOrOrdinal = union(enum) {
221 name: [:0]const u16,
222 ordinal: u16,
223
224 pub fn deinit(self: NameOrOrdinal, allocator: Allocator) void {
225 switch (self) {
226 .name => |name| {
227 allocator.free(name);
228 },
229 .ordinal => {},
230 }
231 }
232
233 /// Returns the full length of the amount of bytes that would be written by `write`
234 /// (e.g. for an ordinal it will return the length including the 0xFFFF indicator)
235 pub fn byteLen(self: NameOrOrdinal) usize {
236 switch (self) {
237 .name => |name| {
238 // + 1 for 0-terminated
239 return (name.len + 1) * @sizeOf(u16);
240 },
241 .ordinal => return 4,
242 }
243 }
244
245 pub fn write(self: NameOrOrdinal, writer: anytype) !void {
246 switch (self) {
247 .name => |name| {
248 for (name[0 .. name.len + 1]) |code_unit| {
249 try writer.writeIntLittle(u16, code_unit);
250 }
251 },
252 .ordinal => |ordinal| {
253 try writer.writeIntLittle(u16, 0xffff);
254 try writer.writeIntLittle(u16, ordinal);
255 },
256 }
257 }
258
259 pub fn writeEmpty(writer: anytype) !void {
260 try writer.writeIntLittle(u16, 0);
261 }
262
263 pub fn fromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
264 if (maybeOrdinalFromString(bytes)) |ordinal| {
265 return ordinal;
266 }
267 return nameFromString(allocator, bytes);
268 }
269
270 pub fn nameFromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
271 // Names have a limit of 256 UTF-16 code units + null terminator
272 var buf = try std.ArrayList(u16).initCapacity(allocator, @min(257, bytes.slice.len));
273 errdefer buf.deinit();
274
275 var i: usize = 0;
276 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
277 if (buf.items.len == 256) break;
278
279 const c = codepoint.value;
280 if (c == Codepoint.invalid) {
281 try buf.append(std.mem.nativeToLittle(u16, '�'));
282 } else if (c < 0x7F) {
283 // ASCII chars in names are always converted to uppercase
284 try buf.append(std.ascii.toUpper(@intCast(c)));
285 } else if (c < 0x10000) {
286 const short: u16 = @intCast(c);
287 try buf.append(std.mem.nativeToLittle(u16, short));
288 } else {
289 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
290 try buf.append(std.mem.nativeToLittle(u16, high));
291
292 // Note: This can cut-off in the middle of a UTF-16 surrogate pair,
293 // i.e. it can make the string end with an unpaired high surrogate
294 if (buf.items.len == 256) break;
295
296 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
297 try buf.append(std.mem.nativeToLittle(u16, low));
298 }
299 }
300
301 return NameOrOrdinal{ .name = try buf.toOwnedSliceSentinel(0) };
302 }
303
304 /// Returns `null` if the bytes do not form a valid number.
305 /// Does not allow non-ASCII digits (which the Win32 RC compiler does allow
306 /// in base 10 numbers, see `maybeNonAsciiOrdinalFromString`).
307 pub fn maybeOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
308 var buf = bytes.slice;
309 var radix: u8 = 10;
310 if (buf.len > 2 and buf[0] == '0') {
311 switch (buf[1]) {
312 '0'...'9' => {},
313 'x', 'X' => {
314 radix = 16;
315 buf = buf[2..];
316 // only the first 4 hex digits matter, anything else is ignored
317 // i.e. 0x12345 is treated as if it were 0x1234
318 buf.len = @min(buf.len, 4);
319 },
320 else => return null,
321 }
322 }
323
324 var i: usize = 0;
325 var result: u16 = 0;
326 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
327 const c = codepoint.value;
328 const digit: u8 = switch (c) {
329 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch switch (radix) {
330 10 => return null,
331 // non-hex-digits are treated as a terminator rather than invalidating
332 // the number (note: if there are no valid hex digits then the result
333 // will be zero which is not treated as a valid number)
334 16 => break,
335 else => unreachable,
336 },
337 else => if (radix == 10) return null else break,
338 };
339
340 if (result != 0) {
341 result *%= radix;
342 }
343 result +%= digit;
344 }
345
346 // Anything that resolves to zero is not interpretted as a number
347 if (result == 0) return null;
348 return NameOrOrdinal{ .ordinal = result };
349 }
350
351 /// The Win32 RC compiler uses `iswdigit` for digit detection for base 10
352 /// numbers, which means that non-ASCII digits are 'accepted' but handled
353 /// in a totally unintuitive manner, leading to arbitrary results.
354 ///
355 /// This function will return the value that such an ordinal 'would' have
356 /// if it was run through the Win32 RC compiler. This allows us to disallow
357 /// non-ASCII digits in number literals but still detect when the Win32
358 /// RC compiler would have allowed them, so that a proper warning/error
359 /// can be emitted.
360 pub fn maybeNonAsciiOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
361 var buf = bytes.slice;
362 const radix = 10;
363 if (buf.len > 2 and buf[0] == '0') {
364 switch (buf[1]) {
365 // We only care about base 10 numbers here
366 'x', 'X' => return null,
367 else => {},
368 }
369 }
370
371 var i: usize = 0;
372 var result: u16 = 0;
373 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
374 const c = codepoint.value;
375 const digit: u16 = digit: {
376 const is_digit = (c >= '0' and c <= '9') or isNonAsciiDigit(c);
377 if (!is_digit) return null;
378 break :digit @intCast(c - '0');
379 };
380
381 if (result != 0) {
382 result *%= radix;
383 }
384 result +%= digit;
385 }
386
387 // Anything that resolves to zero is not interpretted as a number
388 if (result == 0) return null;
389 return NameOrOrdinal{ .ordinal = result };
390 }
391
392 pub fn predefinedResourceType(self: NameOrOrdinal) ?RT {
393 switch (self) {
394 .ordinal => |ordinal| {
395 if (ordinal >= 256) return null;
396 switch (@as(RT, @enumFromInt(ordinal))) {
397 .ACCELERATOR,
398 .ANICURSOR,
399 .ANIICON,
400 .BITMAP,
401 .CURSOR,
402 .DIALOG,
403 .DLGINCLUDE,
404 .DLGINIT,
405 .FONT,
406 .FONTDIR,
407 .GROUP_CURSOR,
408 .GROUP_ICON,
409 .HTML,
410 .ICON,
411 .MANIFEST,
412 .MENU,
413 .MESSAGETABLE,
414 .PLUGPLAY,
415 .RCDATA,
416 .STRING,
417 .TOOLBAR,
418 .VERSION,
419 .VXD,
420 => |rt| return rt,
421 _ => return null,
422 }
423 },
424 .name => return null,
425 }
426 }
427};
428
429fn expectNameOrOrdinal(expected: NameOrOrdinal, actual: NameOrOrdinal) !void {
430 switch (expected) {
431 .name => {
432 if (actual != .name) return error.TestExpectedEqual;
433 try std.testing.expectEqualSlices(u16, expected.name, actual.name);
434 },
435 .ordinal => {
436 if (actual != .ordinal) return error.TestExpectedEqual;
437 try std.testing.expectEqual(expected.ordinal, actual.ordinal);
438 },
439 }
440}
441
442test "NameOrOrdinal" {
443 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
444 defer arena.deinit();
445
446 const allocator = arena.allocator();
447
448 // zero is treated as a string
449 try expectNameOrOrdinal(
450 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0") },
451 try NameOrOrdinal.fromString(allocator, .{ .slice = "0", .code_page = .windows1252 }),
452 );
453 // any non-digit byte invalidates the number
454 try expectNameOrOrdinal(
455 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1A") },
456 try NameOrOrdinal.fromString(allocator, .{ .slice = "1a", .code_page = .windows1252 }),
457 );
458 try expectNameOrOrdinal(
459 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1ÿ") },
460 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\xff", .code_page = .windows1252 }),
461 );
462 try expectNameOrOrdinal(
463 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1€") },
464 try NameOrOrdinal.fromString(allocator, .{ .slice = "1€", .code_page = .utf8 }),
465 );
466 try expectNameOrOrdinal(
467 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1�") },
468 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\x80", .code_page = .utf8 }),
469 );
470 // same with overflow that resolves to 0
471 try expectNameOrOrdinal(
472 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("65536") },
473 try NameOrOrdinal.fromString(allocator, .{ .slice = "65536", .code_page = .windows1252 }),
474 );
475 // hex zero is also treated as a string
476 try expectNameOrOrdinal(
477 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0X0") },
478 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x0", .code_page = .windows1252 }),
479 );
480 // hex numbers work
481 try expectNameOrOrdinal(
482 NameOrOrdinal{ .ordinal = 0x100 },
483 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x100", .code_page = .windows1252 }),
484 );
485 // only the first 4 hex digits matter
486 try expectNameOrOrdinal(
487 NameOrOrdinal{ .ordinal = 0x1234 },
488 try NameOrOrdinal.fromString(allocator, .{ .slice = "0X12345", .code_page = .windows1252 }),
489 );
490 // octal is not supported so it gets treated as a string
491 try expectNameOrOrdinal(
492 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0O1234") },
493 try NameOrOrdinal.fromString(allocator, .{ .slice = "0o1234", .code_page = .windows1252 }),
494 );
495 // overflow wraps
496 try expectNameOrOrdinal(
497 NameOrOrdinal{ .ordinal = @truncate(65635) },
498 try NameOrOrdinal.fromString(allocator, .{ .slice = "65635", .code_page = .windows1252 }),
499 );
500 // non-hex-digits in a hex literal are treated as a terminator
501 try expectNameOrOrdinal(
502 NameOrOrdinal{ .ordinal = 0x4 },
503 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x4n", .code_page = .windows1252 }),
504 );
505 try expectNameOrOrdinal(
506 NameOrOrdinal{ .ordinal = 0xFA },
507 try NameOrOrdinal.fromString(allocator, .{ .slice = "0xFAZ92348", .code_page = .windows1252 }),
508 );
509 // 0 at the start is allowed
510 try expectNameOrOrdinal(
511 NameOrOrdinal{ .ordinal = 50 },
512 try NameOrOrdinal.fromString(allocator, .{ .slice = "050", .code_page = .windows1252 }),
513 );
514 // limit of 256 UTF-16 code units, can cut off between a surrogate pair
515 {
516 var expected = blk: {
517 // the input before the 𐐷 character, but uppercased
518 var expected_u8_bytes = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528QFFL7SHNSIETG0QKLR1UYPBTUV1PMFQRRA0VJDG354GQEDJMUPGPP1W1EXVNTZVEIZ6K3IPQM1AWGEYALMEODYVEZGOD3MFMGEY8FNR4JUETTB1PZDEWSNDRGZUA8SNXP3NGO";
519 var buf: [256:0]u16 = undefined;
520 for (expected_u8_bytes, 0..) |byte, i| {
521 buf[i] = byte;
522 }
523 // surrogate pair that is now orphaned
524 buf[255] = 0xD801;
525 break :blk buf;
526 };
527 try expectNameOrOrdinal(
528 NameOrOrdinal{ .name = &expected },
529 try NameOrOrdinal.fromString(allocator, .{
530 .slice = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528qffL7ShnSIETg0qkLr1UYpbtuv1PMFQRRa0VjDG354GQedJmUPgpp1w1ExVnTzVEiz6K3iPqM1AWGeYALmeODyvEZGOD3MfmGey8fnR4jUeTtB1PzdeWsNDrGzuA8Snxp3NGO𐐷",
531 .code_page = .utf8,
532 }),
533 );
534 }
535}
536
537test "NameOrOrdinal code page awareness" {
538 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
539 defer arena.deinit();
540
541 const allocator = arena.allocator();
542
543 try expectNameOrOrdinal(
544 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("��𐐷") },
545 try NameOrOrdinal.fromString(allocator, .{
546 .slice = "\xF0\x80\x80𐐷",
547 .code_page = .utf8,
548 }),
549 );
550 try expectNameOrOrdinal(
551 // The UTF-8 representation of 𐐷 is 0xF0 0x90 0x90 0xB7. In order to provide valid
552 // UTF-8 to utf8ToUtf16LeStringLiteral, it uses the UTF-8 representation of the codepoint
553 // <U+0x90> which is 0xC2 0x90. The code units in the expected UTF-16 string are:
554 // { 0x00F0, 0x20AC, 0x20AC, 0x00F0, 0x0090, 0x0090, 0x00B7 }
555 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("ð€€ð\xC2\x90\xC2\x90·") },
556 try NameOrOrdinal.fromString(allocator, .{
557 .slice = "\xF0\x80\x80𐐷",
558 .code_page = .windows1252,
559 }),
560 );
561}
562
563/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-accel#members
564/// https://devblogs.microsoft.com/oldnewthing/20070316-00/?p=27593
565pub const AcceleratorModifiers = struct {
566 value: u8 = 0,
567 explicit_ascii_or_virtkey: bool = false,
568
569 pub const ASCII = 0;
570 pub const VIRTKEY = 1;
571 pub const NOINVERT = 1 << 1;
572 pub const SHIFT = 1 << 2;
573 pub const CONTROL = 1 << 3;
574 pub const ALT = 1 << 4;
575 /// Marker for the last accelerator in an accelerator table
576 pub const last_accelerator_in_table = 1 << 7;
577
578 pub fn apply(self: *AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) void {
579 if (modifier == .ascii or modifier == .virtkey) self.explicit_ascii_or_virtkey = true;
580 self.value |= modifierValue(modifier);
581 }
582
583 pub fn isSet(self: AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) bool {
584 // ASCII is set whenever VIRTKEY is not
585 if (modifier == .ascii) return self.value & modifierValue(.virtkey) == 0;
586 return self.value & modifierValue(modifier) != 0;
587 }
588
589 fn modifierValue(modifier: rc.AcceleratorTypeAndOptions) u8 {
590 return switch (modifier) {
591 .ascii => ASCII,
592 .virtkey => VIRTKEY,
593 .noinvert => NOINVERT,
594 .shift => SHIFT,
595 .control => CONTROL,
596 .alt => ALT,
597 };
598 }
599
600 pub fn markLast(self: *AcceleratorModifiers) void {
601 self.value |= last_accelerator_in_table;
602 }
603};
604
605const AcceleratorKeyCodepointTranslator = struct {
606 string_type: literals.StringType,
607
608 pub fn translate(self: @This(), maybe_parsed: ?literals.IterativeStringParser.ParsedCodepoint) ?u21 {
609 const parsed = maybe_parsed orelse return null;
610 if (parsed.codepoint == Codepoint.invalid) return 0xFFFD;
611 if (parsed.from_escaped_integer and self.string_type == .ascii) {
612 return windows1252.toCodepoint(@intCast(parsed.codepoint));
613 }
614 return parsed.codepoint;
615 }
616};
617
618pub const ParseAcceleratorKeyStringError = error{ EmptyAccelerator, AcceleratorTooLong, InvalidControlCharacter, ControlCharacterOutOfRange };
619
620/// Expects bytes to be the full bytes of a string literal token (e.g. including the "" or L"").
621pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: literals.StringParseOptions) (ParseAcceleratorKeyStringError || Allocator.Error)!u16 {
622 if (bytes.slice.len == 0) {
623 return error.EmptyAccelerator;
624 }
625
626 var parser = literals.IterativeStringParser.init(bytes, options);
627 var translator = AcceleratorKeyCodepointTranslator{ .string_type = parser.declared_string_type };
628
629 const first_codepoint = translator.translate(try parser.next()) orelse return error.EmptyAccelerator;
630 // 0 is treated as a terminator, so this is equivalent to an empty string
631 if (first_codepoint == 0) return error.EmptyAccelerator;
632
633 if (first_codepoint == '^') {
634 // Note: Emitting this warning unconditonally whenever ^ is the first character
635 // matches the Win32 RC behavior, but it's questionable whether or not
636 // the warning should be emitted for ^^ since that results in the ASCII
637 // character ^ being written to the .res.
638 if (is_virt and options.diagnostics != null) {
639 try options.diagnostics.?.diagnostics.append(.{
640 .err = .ascii_character_not_equivalent_to_virtual_key_code,
641 .type = .warning,
642 .token = options.diagnostics.?.token,
643 });
644 }
645
646 const c = translator.translate(try parser.next()) orelse return error.InvalidControlCharacter;
647 switch (c) {
648 '^' => return '^', // special case
649 'a'...'z', 'A'...'Z' => return std.ascii.toUpper(@intCast(c)) - 0x40,
650 // Note: The Windows RC compiler allows more than just A-Z, but what it allows
651 // seems to be tied to some sort of Unicode-aware 'is character' function or something.
652 // The full list of codepoints that trigger an out-of-range error can be found here:
653 // https://gist.github.com/squeek502/2e9d0a4728a83eed074ad9785a209fd0
654 // For codepoints >= 0x80 that don't trigger the error, the Windows RC compiler takes the
655 // codepoint and does the `- 0x40` transformation as if it were A-Z which couldn't lead
656 // to anything useable, so there's no point in emulating that behavior--erroring for
657 // all non-[a-zA-Z] makes much more sense and is what was probably intended by the
658 // Windows RC compiler.
659 else => return error.ControlCharacterOutOfRange,
660 }
661 @compileError("this should be unreachable");
662 }
663
664 const second_codepoint = translator.translate(try parser.next());
665
666 var result: u32 = initial_value: {
667 if (first_codepoint >= 0x10000) {
668 if (second_codepoint != null and second_codepoint.? != 0) return error.AcceleratorTooLong;
669 // No idea why it works this way, but this seems to match the Windows RC
670 // behavior for codepoints >= 0x10000
671 const low = @as(u16, @intCast(first_codepoint & 0x3FF)) + 0xDC00;
672 const extra = (first_codepoint - 0x10000) / 0x400;
673 break :initial_value low + extra * 0x100;
674 }
675 break :initial_value first_codepoint;
676 };
677
678 // 0 is treated as a terminator
679 if (second_codepoint != null and second_codepoint.? == 0) return @truncate(result);
680
681 const third_codepoint = translator.translate(try parser.next());
682 // 0 is treated as a terminator, so a 0 in the third position is fine but
683 // anything else is too many codepoints for an accelerator
684 if (third_codepoint != null and third_codepoint.? != 0) return error.AcceleratorTooLong;
685
686 if (second_codepoint) |c| {
687 if (c >= 0x10000) return error.AcceleratorTooLong;
688 result <<= 8;
689 result += c;
690 } else if (is_virt) {
691 switch (result) {
692 'a'...'z' => result -= 0x20, // toUpper
693 else => {},
694 }
695 }
696 return @truncate(result);
697}
698
699test "accelerator keys" {
700 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
701 .{ .slice = "\"^a\"", .code_page = .windows1252 },
702 false,
703 .{},
704 ));
705 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
706 .{ .slice = "\"^A\"", .code_page = .windows1252 },
707 false,
708 .{},
709 ));
710 try std.testing.expectEqual(@as(u16, 26), try parseAcceleratorKeyString(
711 .{ .slice = "\"^Z\"", .code_page = .windows1252 },
712 false,
713 .{},
714 ));
715 try std.testing.expectEqual(@as(u16, '^'), try parseAcceleratorKeyString(
716 .{ .slice = "\"^^\"", .code_page = .windows1252 },
717 false,
718 .{},
719 ));
720
721 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
722 .{ .slice = "\"a\"", .code_page = .windows1252 },
723 false,
724 .{},
725 ));
726 try std.testing.expectEqual(@as(u16, 0x6162), try parseAcceleratorKeyString(
727 .{ .slice = "\"ab\"", .code_page = .windows1252 },
728 false,
729 .{},
730 ));
731
732 try std.testing.expectEqual(@as(u16, 'C'), try parseAcceleratorKeyString(
733 .{ .slice = "\"c\"", .code_page = .windows1252 },
734 true,
735 .{},
736 ));
737 try std.testing.expectEqual(@as(u16, 0x6363), try parseAcceleratorKeyString(
738 .{ .slice = "\"cc\"", .code_page = .windows1252 },
739 true,
740 .{},
741 ));
742
743 // \x00 or any escape that evaluates to zero acts as a terminator, everything past it
744 // is ignored
745 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
746 .{ .slice = "\"a\\0bcdef\"", .code_page = .windows1252 },
747 false,
748 .{},
749 ));
750
751 // \x80 is € in Windows-1252, which is Unicode codepoint 20AC
752 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
753 .{ .slice = "\"\x80\"", .code_page = .windows1252 },
754 false,
755 .{},
756 ));
757 // This depends on the code page, though, with codepage 65001, \x80
758 // on its own is invalid UTF-8 so it gets converted to the replacement character
759 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
760 .{ .slice = "\"\x80\"", .code_page = .utf8 },
761 false,
762 .{},
763 ));
764 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
765 .{ .slice = "\"\x80\x80\"", .code_page = .windows1252 },
766 false,
767 .{},
768 ));
769 // This also behaves the same with escaped characters
770 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
771 .{ .slice = "\"\\x80\"", .code_page = .windows1252 },
772 false,
773 .{},
774 ));
775 // Even with utf8 code page
776 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
777 .{ .slice = "\"\\x80\"", .code_page = .utf8 },
778 false,
779 .{},
780 ));
781 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
782 .{ .slice = "\"\\x80\\x80\"", .code_page = .windows1252 },
783 false,
784 .{},
785 ));
786 // Wide string with the actual characters behaves like the ASCII string version
787 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
788 .{ .slice = "L\"\x80\x80\"", .code_page = .windows1252 },
789 false,
790 .{},
791 ));
792 // But wide string with escapes behaves differently
793 try std.testing.expectEqual(@as(u16, 0x8080), try parseAcceleratorKeyString(
794 .{ .slice = "L\"\\x80\\x80\"", .code_page = .windows1252 },
795 false,
796 .{},
797 ));
798 // and invalid escapes within wide strings get skipped
799 try std.testing.expectEqual(@as(u16, 'z'), try parseAcceleratorKeyString(
800 .{ .slice = "L\"\\Hz\"", .code_page = .windows1252 },
801 false,
802 .{},
803 ));
804
805 // any non-A-Z codepoints are illegal
806 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
807 .{ .slice = "\"^\x83\"", .code_page = .windows1252 },
808 false,
809 .{},
810 ));
811 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
812 .{ .slice = "\"^1\"", .code_page = .windows1252 },
813 false,
814 .{},
815 ));
816 try std.testing.expectError(error.InvalidControlCharacter, parseAcceleratorKeyString(
817 .{ .slice = "\"^\"", .code_page = .windows1252 },
818 false,
819 .{},
820 ));
821 try std.testing.expectError(error.EmptyAccelerator, parseAcceleratorKeyString(
822 .{ .slice = "\"\"", .code_page = .windows1252 },
823 false,
824 .{},
825 ));
826 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
827 .{ .slice = "\"hello\"", .code_page = .windows1252 },
828 false,
829 .{},
830 ));
831 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
832 .{ .slice = "\"^\x80\"", .code_page = .windows1252 },
833 false,
834 .{},
835 ));
836
837 // Invalid UTF-8 gets converted to 0xFFFD, multiple invalids get shifted and added together
838 // The behavior is the same for ascii and wide strings
839 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
840 .{ .slice = "\"\x80\x80\"", .code_page = .utf8 },
841 false,
842 .{},
843 ));
844 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
845 .{ .slice = "L\"\x80\x80\"", .code_page = .utf8 },
846 false,
847 .{},
848 ));
849
850 // Codepoints >= 0x10000
851 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
852 .{ .slice = "\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
853 false,
854 .{},
855 ));
856 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
857 .{ .slice = "L\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
858 false,
859 .{},
860 ));
861 try std.testing.expectEqual(@as(u16, 0x9C01), try parseAcceleratorKeyString(
862 .{ .slice = "\"\xF4\x80\x80\x81\"", .code_page = .utf8 },
863 false,
864 .{},
865 ));
866 // anything before or after a codepoint >= 0x10000 causes an error
867 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
868 .{ .slice = "\"a\xF0\x90\x80\x80\"", .code_page = .utf8 },
869 false,
870 .{},
871 ));
872 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
873 .{ .slice = "\"\xF0\x90\x80\x80a\"", .code_page = .utf8 },
874 false,
875 .{},
876 ));
877}
878
879pub const ForcedOrdinal = struct {
880 pub fn fromBytes(bytes: SourceBytes) u16 {
881 var i: usize = 0;
882 var result: u21 = 0;
883 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
884 const c = switch (codepoint.value) {
885 // Codepoints that would need a surrogate pair in UTF-16 are
886 // broken up into their UTF-16 code units and each code unit
887 // is interpreted as a digit.
888 0x10000...0x10FFFF => {
889 const high = @as(u16, @intCast((codepoint.value - 0x10000) >> 10)) + 0xD800;
890 if (result != 0) result *%= 10;
891 result +%= high -% '0';
892
893 const low = @as(u16, @intCast(codepoint.value & 0x3FF)) + 0xDC00;
894 if (result != 0) result *%= 10;
895 result +%= low -% '0';
896 continue;
897 },
898 Codepoint.invalid => 0xFFFD,
899 else => codepoint.value,
900 };
901 if (result != 0) result *%= 10;
902 result +%= c -% '0';
903 }
904 return @truncate(result);
905 }
906
907 pub fn fromUtf16Le(utf16: [:0]const u16) u16 {
908 var result: u16 = 0;
909 for (utf16) |code_unit| {
910 if (result != 0) result *%= 10;
911 result +%= code_unit -% '0';
912 }
913 return result;
914 }
915};
916
917test "forced ordinal" {
918 try std.testing.expectEqual(@as(u16, 3200), ForcedOrdinal.fromBytes(.{ .slice = "3200", .code_page = .windows1252 }));
919 try std.testing.expectEqual(@as(u16, 0x33), ForcedOrdinal.fromBytes(.{ .slice = "1+1", .code_page = .windows1252 }));
920 try std.testing.expectEqual(@as(u16, 65531), ForcedOrdinal.fromBytes(.{ .slice = "1!", .code_page = .windows1252 }));
921
922 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0\x8C", .code_page = .windows1252 }));
923 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0Œ", .code_page = .utf8 }));
924
925 // invalid UTF-8 gets converted to 0xFFFD (replacement char) and then interpreted as a digit
926 try std.testing.expectEqual(@as(u16, 0xFFCD), ForcedOrdinal.fromBytes(.{ .slice = "0\x81", .code_page = .utf8 }));
927 // codepoints >= 0x10000
928 try std.testing.expectEqual(@as(u16, 0x49F2), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10002}", .code_page = .utf8 }));
929 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10100}", .code_page = .utf8 }));
930
931 // From UTF-16
932 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromUtf16Le(&[_:0]u16{ '0', 'Œ' }));
933 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromUtf16Le(std.unicode.utf8ToUtf16LeStringLiteral("0\u{10100}")));
934}
935
936/// https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
937pub const FixedFileInfo = struct {
938 file_version: Version = .{},
939 product_version: Version = .{},
940 file_flags_mask: u32 = 0,
941 file_flags: u32 = 0,
942 file_os: u32 = 0,
943 file_type: u32 = 0,
944 file_subtype: u32 = 0,
945 file_date: Version = .{}, // TODO: I think this is always all zeroes?
946
947 pub const signature = 0xFEEF04BD;
948 // Note: This corresponds to a version of 1.0
949 pub const version = 0x00010000;
950
951 pub const byte_len = 0x34;
952 pub const key = std.unicode.utf8ToUtf16LeStringLiteral("VS_VERSION_INFO");
953
954 pub const Version = struct {
955 parts: [4]u16 = [_]u16{0} ** 4,
956
957 pub fn mostSignificantCombinedParts(self: Version) u32 {
958 return (@as(u32, self.parts[0]) << 16) + self.parts[1];
959 }
960
961 pub fn leastSignificantCombinedParts(self: Version) u32 {
962 return (@as(u32, self.parts[2]) << 16) + self.parts[3];
963 }
964 };
965
966 pub fn write(self: FixedFileInfo, writer: anytype) !void {
967 try writer.writeIntLittle(u32, signature);
968 try writer.writeIntLittle(u32, version);
969 try writer.writeIntLittle(u32, self.file_version.mostSignificantCombinedParts());
970 try writer.writeIntLittle(u32, self.file_version.leastSignificantCombinedParts());
971 try writer.writeIntLittle(u32, self.product_version.mostSignificantCombinedParts());
972 try writer.writeIntLittle(u32, self.product_version.leastSignificantCombinedParts());
973 try writer.writeIntLittle(u32, self.file_flags_mask);
974 try writer.writeIntLittle(u32, self.file_flags);
975 try writer.writeIntLittle(u32, self.file_os);
976 try writer.writeIntLittle(u32, self.file_type);
977 try writer.writeIntLittle(u32, self.file_subtype);
978 try writer.writeIntLittle(u32, self.file_date.mostSignificantCombinedParts());
979 try writer.writeIntLittle(u32, self.file_date.leastSignificantCombinedParts());
980 }
981};
982
983test "FixedFileInfo.Version" {
984 const version = FixedFileInfo.Version{
985 .parts = .{ 1, 2, 3, 4 },
986 };
987 try std.testing.expectEqual(@as(u32, 0x00010002), version.mostSignificantCombinedParts());
988 try std.testing.expectEqual(@as(u32, 0x00030004), version.leastSignificantCombinedParts());
989}
990
991pub const VersionNode = struct {
992 pub const type_string: u16 = 1;
993 pub const type_binary: u16 = 0;
994};
995
996pub const MenuItemFlags = struct {
997 value: u16 = 0,
998
999 pub fn apply(self: *MenuItemFlags, option: rc.MenuItem.Option) void {
1000 self.value |= optionValue(option);
1001 }
1002
1003 pub fn isSet(self: MenuItemFlags, option: rc.MenuItem.Option) bool {
1004 return self.value & optionValue(option) != 0;
1005 }
1006
1007 fn optionValue(option: rc.MenuItem.Option) u16 {
1008 return @intCast(switch (option) {
1009 .checked => MF.CHECKED,
1010 .grayed => MF.GRAYED,
1011 .help => MF.HELP,
1012 .inactive => MF.DISABLED,
1013 .menubarbreak => MF.MENUBARBREAK,
1014 .menubreak => MF.MENUBREAK,
1015 });
1016 }
1017
1018 pub fn markLast(self: *MenuItemFlags) void {
1019 self.value |= @intCast(MF.END);
1020 }
1021};
1022
1023/// Menu Flags from WinUser.h
1024/// This is not complete, it only contains what is needed
1025pub const MF = struct {
1026 pub const GRAYED: u32 = 0x00000001;
1027 pub const DISABLED: u32 = 0x00000002;
1028 pub const CHECKED: u32 = 0x00000008;
1029 pub const POPUP: u32 = 0x00000010;
1030 pub const MENUBARBREAK: u32 = 0x00000020;
1031 pub const MENUBREAK: u32 = 0x00000040;
1032 pub const HELP: u32 = 0x00004000;
1033 pub const END: u32 = 0x00000080;
1034};
1035
1036/// Window Styles from WinUser.h
1037pub const WS = struct {
1038 pub const OVERLAPPED: u32 = 0x00000000;
1039 pub const POPUP: u32 = 0x80000000;
1040 pub const CHILD: u32 = 0x40000000;
1041 pub const MINIMIZE: u32 = 0x20000000;
1042 pub const VISIBLE: u32 = 0x10000000;
1043 pub const DISABLED: u32 = 0x08000000;
1044 pub const CLIPSIBLINGS: u32 = 0x04000000;
1045 pub const CLIPCHILDREN: u32 = 0x02000000;
1046 pub const MAXIMIZE: u32 = 0x01000000;
1047 pub const CAPTION: u32 = BORDER | DLGFRAME;
1048 pub const BORDER: u32 = 0x00800000;
1049 pub const DLGFRAME: u32 = 0x00400000;
1050 pub const VSCROLL: u32 = 0x00200000;
1051 pub const HSCROLL: u32 = 0x00100000;
1052 pub const SYSMENU: u32 = 0x00080000;
1053 pub const THICKFRAME: u32 = 0x00040000;
1054 pub const GROUP: u32 = 0x00020000;
1055 pub const TABSTOP: u32 = 0x00010000;
1056
1057 pub const MINIMIZEBOX: u32 = 0x00020000;
1058 pub const MAXIMIZEBOX: u32 = 0x00010000;
1059
1060 pub const TILED: u32 = OVERLAPPED;
1061 pub const ICONIC: u32 = MINIMIZE;
1062 pub const SIZEBOX: u32 = THICKFRAME;
1063 pub const TILEDWINDOW: u32 = OVERLAPPEDWINDOW;
1064
1065 // Common Window Styles
1066 pub const OVERLAPPEDWINDOW: u32 = OVERLAPPED | CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX;
1067 pub const POPUPWINDOW: u32 = POPUP | BORDER | SYSMENU;
1068 pub const CHILDWINDOW: u32 = CHILD;
1069};
1070
1071/// Dialog Box Template Styles from WinUser.h
1072pub const DS = struct {
1073 pub const SETFONT: u32 = 0x40;
1074};
1075
1076/// Button Control Styles from WinUser.h
1077/// This is not complete, it only contains what is needed
1078pub const BS = struct {
1079 pub const PUSHBUTTON: u32 = 0x00000000;
1080 pub const DEFPUSHBUTTON: u32 = 0x00000001;
1081 pub const CHECKBOX: u32 = 0x00000002;
1082 pub const AUTOCHECKBOX: u32 = 0x00000003;
1083 pub const RADIOBUTTON: u32 = 0x00000004;
1084 pub const @"3STATE": u32 = 0x00000005;
1085 pub const AUTO3STATE: u32 = 0x00000006;
1086 pub const GROUPBOX: u32 = 0x00000007;
1087 pub const USERBUTTON: u32 = 0x00000008;
1088 pub const AUTORADIOBUTTON: u32 = 0x00000009;
1089 pub const PUSHBOX: u32 = 0x0000000A;
1090 pub const OWNERDRAW: u32 = 0x0000000B;
1091 pub const TYPEMASK: u32 = 0x0000000F;
1092 pub const LEFTTEXT: u32 = 0x00000020;
1093};
1094
1095/// Static Control Constants from WinUser.h
1096/// This is not complete, it only contains what is needed
1097pub const SS = struct {
1098 pub const LEFT: u32 = 0x00000000;
1099 pub const CENTER: u32 = 0x00000001;
1100 pub const RIGHT: u32 = 0x00000002;
1101 pub const ICON: u32 = 0x00000003;
1102};
1103
1104/// Listbox Styles from WinUser.h
1105/// This is not complete, it only contains what is needed
1106pub const LBS = struct {
1107 pub const NOTIFY: u32 = 0x0001;
1108};
src/resinator/source_mapping.zig created+684
......@@ -0,0 +1,684 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter;
4const parseQuotedAsciiString = @import("literals.zig").parseQuotedAsciiString;
5const lex = @import("lex.zig");
6
7pub const ParseLineCommandsResult = struct {
8 result: []u8,
9 mappings: SourceMappings,
10};
11
12const CurrentMapping = struct {
13 line_num: usize = 1,
14 filename: std.ArrayListUnmanaged(u8) = .{},
15 pending: bool = true,
16 ignore_contents: bool = false,
17};
18
19pub const ParseAndRemoveLineCommandsOptions = struct {
20 initial_filename: ?[]const u8 = null,
21};
22
23/// Parses and removes #line commands as well as all source code that is within a file
24/// with .c or .h extensions.
25///
26/// > RC treats files with the .c and .h extensions in a special manner. It
27/// > assumes that a file with one of these extensions does not contain
28/// > resources. If a file has the .c or .h file name extension, RC ignores all
29/// > lines in the file except the preprocessor directives. Therefore, to
30/// > include a file that contains resources in another resource script, give
31/// > the file to be included an extension other than .c or .h.
32/// from https://learn.microsoft.com/en-us/windows/win32/menurc/preprocessor-directives
33///
34/// Returns a slice of `buf` with the aforementioned stuff removed as well as a mapping
35/// between the lines and their corresponding lines in their original files.
36///
37/// `buf` must be at least as long as `source`
38/// In-place transformation is supported (i.e. `source` and `buf` can be the same slice)
39///
40/// If `options.initial_filename` is provided, that filename is guaranteed to be
41/// within the `mappings.files` table and `root_filename_offset` will be set appropriately.
42pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult {
43 var parse_result = ParseLineCommandsResult{
44 .result = undefined,
45 .mappings = .{},
46 };
47 errdefer parse_result.mappings.deinit(allocator);
48
49 var current_mapping: CurrentMapping = .{};
50 defer current_mapping.filename.deinit(allocator);
51
52 if (options.initial_filename) |initial_filename| {
53 try current_mapping.filename.appendSlice(allocator, initial_filename);
54 parse_result.mappings.root_filename_offset = try parse_result.mappings.files.put(allocator, initial_filename);
55 }
56
57 std.debug.assert(buf.len >= source.len);
58 var result = UncheckedSliceWriter{ .slice = buf };
59 const State = enum {
60 line_start,
61 preprocessor,
62 non_preprocessor,
63 };
64 var state: State = .line_start;
65 var index: usize = 0;
66 var pending_start: ?usize = null;
67 var preprocessor_start: usize = 0;
68 var line_number: usize = 1;
69 while (index < source.len) : (index += 1) {
70 const c = source[index];
71 switch (state) {
72 .line_start => switch (c) {
73 '#' => {
74 preprocessor_start = index;
75 state = .preprocessor;
76 if (pending_start == null) {
77 pending_start = index;
78 }
79 },
80 '\r', '\n' => {
81 const is_crlf = formsLineEndingPair(source, c, index + 1);
82 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
83 if (!current_mapping.ignore_contents) {
84 result.write(c);
85 if (is_crlf) result.write(source[index + 1]);
86 line_number += 1;
87 }
88 if (is_crlf) index += 1;
89 pending_start = null;
90 },
91 ' ', '\t', '\x0b', '\x0c' => {
92 if (pending_start == null) {
93 pending_start = index;
94 }
95 },
96 else => {
97 state = .non_preprocessor;
98 if (pending_start != null) {
99 if (!current_mapping.ignore_contents) {
100 result.writeSlice(source[pending_start.? .. index + 1]);
101 }
102 pending_start = null;
103 continue;
104 }
105 if (!current_mapping.ignore_contents) {
106 result.write(c);
107 }
108 },
109 },
110 .preprocessor => switch (c) {
111 '\r', '\n' => {
112 // Now that we have the full line we can decide what to do with it
113 const preprocessor_str = source[preprocessor_start..index];
114 const is_crlf = formsLineEndingPair(source, c, index + 1);
115 if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
116 try handleLineCommand(allocator, preprocessor_str, &current_mapping);
117 } else {
118 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
119 if (!current_mapping.ignore_contents) {
120 const line_ending_len: usize = if (is_crlf) 2 else 1;
121 result.writeSlice(source[pending_start.? .. index + line_ending_len]);
122 line_number += 1;
123 }
124 }
125 if (is_crlf) index += 1;
126 state = .line_start;
127 pending_start = null;
128 },
129 else => {},
130 },
131 .non_preprocessor => switch (c) {
132 '\r', '\n' => {
133 const is_crlf = formsLineEndingPair(source, c, index + 1);
134 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
135 if (!current_mapping.ignore_contents) {
136 result.write(c);
137 if (is_crlf) result.write(source[index + 1]);
138 line_number += 1;
139 }
140 if (is_crlf) index += 1;
141 state = .line_start;
142 pending_start = null;
143 },
144 else => {
145 if (!current_mapping.ignore_contents) {
146 result.write(c);
147 }
148 },
149 },
150 }
151 } else {
152 switch (state) {
153 .line_start => {},
154 .non_preprocessor => {
155 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
156 },
157 .preprocessor => {
158 // Now that we have the full line we can decide what to do with it
159 const preprocessor_str = source[preprocessor_start..index];
160 if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
161 try handleLineCommand(allocator, preprocessor_str, &current_mapping);
162 } else {
163 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
164 if (!current_mapping.ignore_contents) {
165 result.writeSlice(source[pending_start.?..index]);
166 }
167 }
168 },
169 }
170 }
171
172 parse_result.result = result.getWritten();
173
174 // Remove whitespace from the end of the result. This avoids issues when the
175 // preprocessor adds a newline to the end of the file, since then the
176 // post-preprocessed source could have more lines than the corresponding input source and
177 // the inserted line can't be mapped to any lines in the original file.
178 // There's no way that whitespace at the end of a file can affect the parsing
179 // of the RC script so this is okay to do unconditionally.
180 // TODO: There might be a better way around this
181 while (parse_result.result.len > 0 and std.ascii.isWhitespace(parse_result.result[parse_result.result.len - 1])) {
182 parse_result.result.len -= 1;
183 }
184
185 // If there have been no line mappings at all, then we're dealing with an empty file.
186 // In this case, we want to fake a line mapping just so that we return something
187 // that is useable in the same way that a non-empty mapping would be.
188 if (parse_result.mappings.mapping.items.len == 0) {
189 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
190 }
191
192 return parse_result;
193}
194
195/// Note: This should function the same as lex.LineHandler.currentIndexFormsLineEndingPair
196pub fn formsLineEndingPair(source: []const u8, line_ending: u8, next_index: usize) bool {
197 if (next_index >= source.len) return false;
198
199 const next_ending = source[next_index];
200 if (next_ending != '\r' and next_ending != '\n') return false;
201
202 // can't be \n\n or \r\r
203 if (line_ending == next_ending) return false;
204
205 return true;
206}
207
208pub fn handleLineEnd(allocator: Allocator, post_processed_line_number: usize, mapping: *SourceMappings, current_mapping: *CurrentMapping) !void {
209 const filename_offset = try mapping.files.put(allocator, current_mapping.filename.items);
210
211 try mapping.set(allocator, post_processed_line_number, .{
212 .start_line = current_mapping.line_num,
213 .end_line = current_mapping.line_num,
214 .filename_offset = filename_offset,
215 });
216
217 current_mapping.line_num += 1;
218 current_mapping.pending = false;
219}
220
221// TODO: Might want to provide diagnostics on invalid line commands instead of just returning
222pub fn handleLineCommand(allocator: Allocator, line_command: []const u8, current_mapping: *CurrentMapping) error{OutOfMemory}!void {
223 // TODO: Are there other whitespace characters that should be included?
224 var tokenizer = std.mem.tokenize(u8, line_command, " \t");
225 const line_directive = tokenizer.next() orelse return; // #line
226 if (!std.mem.eql(u8, line_directive, "#line")) return;
227 const linenum_str = tokenizer.next() orelse return;
228 const linenum = std.fmt.parseUnsigned(usize, linenum_str, 10) catch return;
229
230 var filename_literal = tokenizer.rest();
231 while (filename_literal.len > 0 and std.ascii.isWhitespace(filename_literal[filename_literal.len - 1])) {
232 filename_literal.len -= 1;
233 }
234 if (filename_literal.len < 2) return;
235 const is_quoted = filename_literal[0] == '"' and filename_literal[filename_literal.len - 1] == '"';
236 if (!is_quoted) return;
237 const filename = parseFilename(allocator, filename_literal[1 .. filename_literal.len - 1]) catch |err| switch (err) {
238 error.OutOfMemory => |e| return e,
239 else => return,
240 };
241 defer allocator.free(filename);
242
243 current_mapping.line_num = linenum;
244 current_mapping.filename.clearRetainingCapacity();
245 try current_mapping.filename.appendSlice(allocator, filename);
246 current_mapping.pending = true;
247 current_mapping.ignore_contents = std.ascii.endsWithIgnoreCase(filename, ".c") or std.ascii.endsWithIgnoreCase(filename, ".h");
248}
249
250pub fn parseAndRemoveLineCommandsAlloc(allocator: Allocator, source: []const u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult {
251 var buf = try allocator.alloc(u8, source.len);
252 errdefer allocator.free(buf);
253 var result = try parseAndRemoveLineCommands(allocator, source, buf, options);
254 result.result = try allocator.realloc(buf, result.result.len);
255 return result;
256}
257
258/// C-style string parsing with a few caveats:
259/// - The str cannot contain newlines or carriage returns
260/// - Hex and octal escape are limited to u8
261/// - No handling/support for L, u, or U prefixed strings
262/// - The start and end double quotes should be omitted from the `str`
263/// - Other than the above, does not assume any validity of the strings (i.e. there
264/// may be unescaped double quotes within the str) and will return error.InvalidString
265/// on any problems found.
266///
267/// The result is a UTF-8 encoded string.
268fn parseFilename(allocator: Allocator, str: []const u8) error{ OutOfMemory, InvalidString }![]u8 {
269 const State = enum {
270 string,
271 escape,
272 escape_hex,
273 escape_octal,
274 escape_u,
275 };
276
277 var filename = try std.ArrayList(u8).initCapacity(allocator, str.len);
278 errdefer filename.deinit();
279 var state: State = .string;
280 var index: usize = 0;
281 var escape_len: usize = undefined;
282 var escape_val: u64 = undefined;
283 var escape_expected_len: u8 = undefined;
284 while (index < str.len) : (index += 1) {
285 const c = str[index];
286 switch (state) {
287 .string => switch (c) {
288 '\\' => state = .escape,
289 '"' => return error.InvalidString,
290 else => filename.appendAssumeCapacity(c),
291 },
292 .escape => switch (c) {
293 '\'', '"', '\\', '?', 'n', 'r', 't', 'a', 'b', 'e', 'f', 'v' => {
294 const escaped_c = switch (c) {
295 '\'', '"', '\\', '?' => c,
296 'n' => '\n',
297 'r' => '\r',
298 't' => '\t',
299 'a' => '\x07',
300 'b' => '\x08',
301 'e' => '\x1b', // non-standard
302 'f' => '\x0c',
303 'v' => '\x0b',
304 else => unreachable,
305 };
306 filename.appendAssumeCapacity(escaped_c);
307 state = .string;
308 },
309 'x' => {
310 escape_val = 0;
311 escape_len = 0;
312 state = .escape_hex;
313 },
314 '0'...'7' => {
315 escape_val = std.fmt.charToDigit(c, 8) catch unreachable;
316 escape_len = 1;
317 state = .escape_octal;
318 },
319 'u' => {
320 escape_val = 0;
321 escape_len = 0;
322 state = .escape_u;
323 escape_expected_len = 4;
324 },
325 'U' => {
326 escape_val = 0;
327 escape_len = 0;
328 state = .escape_u;
329 escape_expected_len = 8;
330 },
331 else => return error.InvalidString,
332 },
333 .escape_hex => switch (c) {
334 '0'...'9', 'a'...'f', 'A'...'F' => {
335 const digit = std.fmt.charToDigit(c, 16) catch unreachable;
336 if (escape_val != 0) escape_val = std.math.mul(u8, @as(u8, @intCast(escape_val)), 16) catch return error.InvalidString;
337 escape_val = std.math.add(u8, @as(u8, @intCast(escape_val)), digit) catch return error.InvalidString;
338 escape_len += 1;
339 },
340 else => {
341 if (escape_len == 0) return error.InvalidString;
342 filename.appendAssumeCapacity(@intCast(escape_val));
343 state = .string;
344 index -= 1; // reconsume
345 },
346 },
347 .escape_octal => switch (c) {
348 '0'...'7' => {
349 const digit = std.fmt.charToDigit(c, 8) catch unreachable;
350 if (escape_val != 0) escape_val = std.math.mul(u8, @as(u8, @intCast(escape_val)), 8) catch return error.InvalidString;
351 escape_val = std.math.add(u8, @as(u8, @intCast(escape_val)), digit) catch return error.InvalidString;
352 escape_len += 1;
353 if (escape_len == 3) {
354 filename.appendAssumeCapacity(@intCast(escape_val));
355 state = .string;
356 }
357 },
358 else => {
359 if (escape_len == 0) return error.InvalidString;
360 filename.appendAssumeCapacity(@intCast(escape_val));
361 state = .string;
362 index -= 1; // reconsume
363 },
364 },
365 .escape_u => switch (c) {
366 '0'...'9', 'a'...'f', 'A'...'F' => {
367 const digit = std.fmt.charToDigit(c, 16) catch unreachable;
368 if (escape_val != 0) escape_val = std.math.mul(u21, @as(u21, @intCast(escape_val)), 16) catch return error.InvalidString;
369 escape_val = std.math.add(u21, @as(u21, @intCast(escape_val)), digit) catch return error.InvalidString;
370 escape_len += 1;
371 if (escape_len == escape_expected_len) {
372 var buf: [4]u8 = undefined;
373 const utf8_len = std.unicode.utf8Encode(@intCast(escape_val), &buf) catch return error.InvalidString;
374 filename.appendSliceAssumeCapacity(buf[0..utf8_len]);
375 state = .string;
376 }
377 },
378 // Requires escape_expected_len valid hex digits
379 else => return error.InvalidString,
380 },
381 }
382 } else {
383 switch (state) {
384 .string => {},
385 .escape, .escape_u => return error.InvalidString,
386 .escape_hex => {
387 if (escape_len == 0) return error.InvalidString;
388 filename.appendAssumeCapacity(@intCast(escape_val));
389 },
390 .escape_octal => {
391 filename.appendAssumeCapacity(@intCast(escape_val));
392 },
393 }
394 }
395
396 return filename.toOwnedSlice();
397}
398
399fn testParseFilename(expected: []const u8, input: []const u8) !void {
400 const parsed = try parseFilename(std.testing.allocator, input);
401 defer std.testing.allocator.free(parsed);
402
403 return std.testing.expectEqualSlices(u8, expected, parsed);
404}
405
406test parseFilename {
407 try testParseFilename("'\"?\\\t\n\r\x11", "\\'\\\"\\?\\\\\\t\\n\\r\\x11");
408 try testParseFilename("\xABz\x53", "\\xABz\\123");
409 try testParseFilename("⚡⚡", "\\u26A1\\U000026A1");
410 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\""));
411 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\"));
412 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\u"));
413 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\U"));
414 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\x"));
415 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\xZZ"));
416 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\xABCDEF"));
417 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\777"));
418}
419
420pub const SourceMappings = struct {
421 /// line number -> span where the index is (line number - 1)
422 mapping: std.ArrayListUnmanaged(SourceSpan) = .{},
423 files: StringTable = .{},
424 /// The default assumes that the first filename added is the root file.
425 /// The value should be set to the correct offset if that assumption does not hold.
426 root_filename_offset: u32 = 0,
427
428 pub const SourceSpan = struct {
429 start_line: usize,
430 end_line: usize,
431 filename_offset: u32,
432 };
433
434 pub fn deinit(self: *SourceMappings, allocator: Allocator) void {
435 self.files.deinit(allocator);
436 self.mapping.deinit(allocator);
437 }
438
439 pub fn set(self: *SourceMappings, allocator: Allocator, line_num: usize, span: SourceSpan) !void {
440 var ptr = try self.expandAndGet(allocator, line_num);
441 ptr.* = span;
442 }
443
444 pub fn has(self: *SourceMappings, line_num: usize) bool {
445 return self.mapping.items.len >= line_num;
446 }
447
448 /// Note: `line_num` is 1-indexed
449 pub fn get(self: SourceMappings, line_num: usize) SourceSpan {
450 return self.mapping.items[line_num - 1];
451 }
452
453 pub fn getPtr(self: SourceMappings, line_num: usize) *SourceSpan {
454 return &self.mapping.items[line_num - 1];
455 }
456
457 /// Expands the number of lines in the mapping to include the requested
458 /// line number (if necessary) and returns a pointer to the value at that
459 /// line number.
460 ///
461 /// Note: `line_num` is 1-indexed
462 pub fn expandAndGet(self: *SourceMappings, allocator: Allocator, line_num: usize) !*SourceSpan {
463 try self.mapping.resize(allocator, line_num);
464 return &self.mapping.items[line_num - 1];
465 }
466
467 pub fn collapse(self: *SourceMappings, line_num: usize, num_following_lines_to_collapse: usize) void {
468 std.debug.assert(num_following_lines_to_collapse > 0);
469
470 var span_to_collapse_into = self.getPtr(line_num);
471 const last_collapsed_span = self.get(line_num + num_following_lines_to_collapse);
472 span_to_collapse_into.end_line = last_collapsed_span.end_line;
473
474 const after_collapsed_start = line_num + num_following_lines_to_collapse;
475 const new_num_lines = self.mapping.items.len - num_following_lines_to_collapse;
476 std.mem.copy(SourceSpan, self.mapping.items[line_num..new_num_lines], self.mapping.items[after_collapsed_start..]);
477
478 self.mapping.items.len = new_num_lines;
479 }
480
481 /// Returns true if the line is from the main/root file (i.e. not a file that has been
482 /// `#include`d).
483 pub fn isRootFile(self: *SourceMappings, line_num: usize) bool {
484 const line_mapping = self.get(line_num);
485 if (line_mapping.filename_offset == self.root_filename_offset) return true;
486 return false;
487 }
488};
489
490test "SourceMappings collapse" {
491 const allocator = std.testing.allocator;
492
493 var mappings = SourceMappings{};
494 defer mappings.deinit(allocator);
495 const filename_offset = try mappings.files.put(allocator, "test.rc");
496
497 try mappings.set(allocator, 1, .{ .start_line = 1, .end_line = 1, .filename_offset = filename_offset });
498 try mappings.set(allocator, 2, .{ .start_line = 2, .end_line = 3, .filename_offset = filename_offset });
499 try mappings.set(allocator, 3, .{ .start_line = 4, .end_line = 4, .filename_offset = filename_offset });
500 try mappings.set(allocator, 4, .{ .start_line = 5, .end_line = 5, .filename_offset = filename_offset });
501
502 mappings.collapse(1, 2);
503
504 try std.testing.expectEqual(@as(usize, 2), mappings.mapping.items.len);
505 try std.testing.expectEqual(@as(usize, 4), mappings.mapping.items[0].end_line);
506 try std.testing.expectEqual(@as(usize, 5), mappings.mapping.items[1].end_line);
507}
508
509/// Same thing as StringTable in Zig's src/Wasm.zig
510pub const StringTable = struct {
511 data: std.ArrayListUnmanaged(u8) = .{},
512 map: std.HashMapUnmanaged(u32, void, std.hash_map.StringIndexContext, std.hash_map.default_max_load_percentage) = .{},
513
514 pub fn deinit(self: *StringTable, allocator: Allocator) void {
515 self.data.deinit(allocator);
516 self.map.deinit(allocator);
517 }
518
519 pub fn put(self: *StringTable, allocator: Allocator, value: []const u8) !u32 {
520 const result = try self.map.getOrPutContextAdapted(
521 allocator,
522 value,
523 std.hash_map.StringIndexAdapter{ .bytes = &self.data },
524 .{ .bytes = &self.data },
525 );
526 if (result.found_existing) {
527 return result.key_ptr.*;
528 }
529
530 try self.data.ensureUnusedCapacity(allocator, value.len + 1);
531 const offset: u32 = @intCast(self.data.items.len);
532
533 self.data.appendSliceAssumeCapacity(value);
534 self.data.appendAssumeCapacity(0);
535
536 result.key_ptr.* = offset;
537
538 return offset;
539 }
540
541 pub fn get(self: StringTable, offset: u32) []const u8 {
542 std.debug.assert(offset < self.data.items.len);
543 return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(self.data.items.ptr + offset)), 0);
544 }
545
546 pub fn getOffset(self: *StringTable, value: []const u8) ?u32 {
547 return self.map.getKeyAdapted(
548 value,
549 std.hash_map.StringIndexAdapter{ .bytes = &self.data },
550 );
551 }
552};
553
554const ExpectedSourceSpan = struct {
555 start_line: usize,
556 end_line: usize,
557 filename: []const u8,
558};
559
560fn testParseAndRemoveLineCommands(
561 expected: []const u8,
562 comptime expected_spans: []const ExpectedSourceSpan,
563 source: []const u8,
564 options: ParseAndRemoveLineCommandsOptions,
565) !void {
566 var results = try parseAndRemoveLineCommandsAlloc(std.testing.allocator, source, options);
567 defer std.testing.allocator.free(results.result);
568 defer results.mappings.deinit(std.testing.allocator);
569
570 try std.testing.expectEqualStrings(expected, results.result);
571
572 expectEqualMappings(expected_spans, results.mappings) catch |err| {
573 std.debug.print("\nexpected mappings:\n", .{});
574 for (expected_spans, 0..) |span, i| {
575 const line_num = i + 1;
576 std.debug.print("{}: {s}:{}-{}\n", .{ line_num, span.filename, span.start_line, span.end_line });
577 }
578 std.debug.print("\nactual mappings:\n", .{});
579 for (results.mappings.mapping.items, 0..) |span, i| {
580 const line_num = i + 1;
581 const filename = results.mappings.files.get(span.filename_offset);
582 std.debug.print("{}: {s}:{}-{}\n", .{ line_num, filename, span.start_line, span.end_line });
583 }
584 std.debug.print("\n", .{});
585 return err;
586 };
587}
588
589fn expectEqualMappings(expected_spans: []const ExpectedSourceSpan, mappings: SourceMappings) !void {
590 try std.testing.expectEqual(expected_spans.len, mappings.mapping.items.len);
591 for (expected_spans, 0..) |expected_span, i| {
592 const line_num = i + 1;
593 const span = mappings.get(line_num);
594 const filename = mappings.files.get(span.filename_offset);
595 try std.testing.expectEqual(expected_span.start_line, span.start_line);
596 try std.testing.expectEqual(expected_span.end_line, span.end_line);
597 try std.testing.expectEqualStrings(expected_span.filename, filename);
598 }
599}
600
601test "basic" {
602 try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{
603 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
604 }, "#line 1 \"blah.rc\"", .{});
605}
606
607test "only removes line commands" {
608 try testParseAndRemoveLineCommands(
609 \\#pragma code_page(65001)
610 , &[_]ExpectedSourceSpan{
611 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
612 },
613 \\#line 1 "blah.rc"
614 \\#pragma code_page(65001)
615 , .{});
616}
617
618test "whitespace and line endings" {
619 try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{
620 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
621 }, "#line \t 1 \t \"blah.rc\"\r\n", .{});
622}
623
624test "example" {
625 try testParseAndRemoveLineCommands(
626 \\
627 \\included RCDATA {"hello"}
628 , &[_]ExpectedSourceSpan{
629 .{ .start_line = 1, .end_line = 1, .filename = "./included.rc" },
630 .{ .start_line = 2, .end_line = 2, .filename = "./included.rc" },
631 },
632 \\#line 1 "rcdata.rc"
633 \\#line 1 "<built-in>"
634 \\#line 1 "<built-in>"
635 \\#line 355 "<built-in>"
636 \\#line 1 "<command line>"
637 \\#line 1 "<built-in>"
638 \\#line 1 "rcdata.rc"
639 \\#line 1 "./header.h"
640 \\
641 \\
642 \\2 RCDATA {"blah"}
643 \\
644 \\
645 \\#line 1 "./included.rc"
646 \\
647 \\included RCDATA {"hello"}
648 \\#line 7 "./header.h"
649 \\#line 1 "rcdata.rc"
650 , .{});
651}
652
653test "CRLF and other line endings" {
654 try testParseAndRemoveLineCommands(
655 "hello\r\n#pragma code_page(65001)\r\nworld",
656 &[_]ExpectedSourceSpan{
657 .{ .start_line = 1, .end_line = 1, .filename = "crlf.rc" },
658 .{ .start_line = 2, .end_line = 2, .filename = "crlf.rc" },
659 .{ .start_line = 3, .end_line = 3, .filename = "crlf.rc" },
660 },
661 "#line 1 \"crlf.rc\"\r\n#line 1 \"<built-in>\"\r#line 1 \"crlf.rc\"\n\rhello\r\n#pragma code_page(65001)\r\nworld\r\n",
662 .{},
663 );
664}
665
666test "no line commands" {
667 try testParseAndRemoveLineCommands(
668 \\1 RCDATA {"blah"}
669 \\2 RCDATA {"blah"}
670 , &[_]ExpectedSourceSpan{
671 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
672 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
673 },
674 \\1 RCDATA {"blah"}
675 \\2 RCDATA {"blah"}
676 , .{ .initial_filename = "blah.rc" });
677}
678
679test "in place" {
680 var mut_source = "#line 1 \"blah.rc\"".*;
681 var result = try parseAndRemoveLineCommands(std.testing.allocator, &mut_source, &mut_source, .{});
682 defer result.mappings.deinit(std.testing.allocator);
683 try std.testing.expectEqualStrings("", result.result);
684}
src/resinator/utils.zig created+83
......@@ -0,0 +1,83 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4/// Like std.io.FixedBufferStream but does no bounds checking
5pub const UncheckedSliceWriter = struct {
6 const Self = @This();
7
8 pos: usize = 0,
9 slice: []u8,
10
11 pub fn write(self: *Self, char: u8) void {
12 self.slice[self.pos] = char;
13 self.pos += 1;
14 }
15
16 pub fn writeSlice(self: *Self, slice: []const u8) void {
17 for (slice) |c| {
18 self.write(c);
19 }
20 }
21
22 pub fn getWritten(self: Self) []u8 {
23 return self.slice[0..self.pos];
24 }
25};
26
27/// Cross-platform 'std.fs.Dir.openFile' wrapper that will always return IsDir if
28/// a directory is attempted to be opened.
29/// TODO: Remove once https://github.com/ziglang/zig/issues/5732 is addressed.
30pub fn openFileNotDir(cwd: std.fs.Dir, path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File {
31 const file = try cwd.openFile(path, flags);
32 errdefer file.close();
33 // https://github.com/ziglang/zig/issues/5732
34 if (builtin.os.tag != .windows) {
35 const stat = try file.stat();
36
37 if (stat.kind == .directory)
38 return error.IsDir;
39 }
40 return file;
41}
42
43/// Emulates the Windows implementation of `iswdigit`, but only returns true
44/// for the non-ASCII digits that `iswdigit` on Windows would return true for.
45pub fn isNonAsciiDigit(c: u21) bool {
46 return switch (c) {
47 '²',
48 '³',
49 '¹',
50 '\u{660}'...'\u{669}',
51 '\u{6F0}'...'\u{6F9}',
52 '\u{7C0}'...'\u{7C9}',
53 '\u{966}'...'\u{96F}',
54 '\u{9E6}'...'\u{9EF}',
55 '\u{A66}'...'\u{A6F}',
56 '\u{AE6}'...'\u{AEF}',
57 '\u{B66}'...'\u{B6F}',
58 '\u{BE6}'...'\u{BEF}',
59 '\u{C66}'...'\u{C6F}',
60 '\u{CE6}'...'\u{CEF}',
61 '\u{D66}'...'\u{D6F}',
62 '\u{E50}'...'\u{E59}',
63 '\u{ED0}'...'\u{ED9}',
64 '\u{F20}'...'\u{F29}',
65 '\u{1040}'...'\u{1049}',
66 '\u{1090}'...'\u{1099}',
67 '\u{17E0}'...'\u{17E9}',
68 '\u{1810}'...'\u{1819}',
69 '\u{1946}'...'\u{194F}',
70 '\u{19D0}'...'\u{19D9}',
71 '\u{1B50}'...'\u{1B59}',
72 '\u{1BB0}'...'\u{1BB9}',
73 '\u{1C40}'...'\u{1C49}',
74 '\u{1C50}'...'\u{1C59}',
75 '\u{A620}'...'\u{A629}',
76 '\u{A8D0}'...'\u{A8D9}',
77 '\u{A900}'...'\u{A909}',
78 '\u{AA50}'...'\u{AA59}',
79 '\u{FF10}'...'\u{FF19}',
80 => true,
81 else => false,
82 };
83}
src/resinator/windows1252.zig created+588
......@@ -0,0 +1,588 @@
1const std = @import("std");
2
3pub fn windows1252ToUtf8Stream(writer: anytype, reader: anytype) !usize {
4 var bytes_written: usize = 0;
5 var utf8_buf: [3]u8 = undefined;
6 while (true) {
7 const c = reader.readByte() catch |err| switch (err) {
8 error.EndOfStream => return bytes_written,
9 else => |e| return e,
10 };
11 const codepoint = toCodepoint(c);
12 if (codepoint <= 0x7F) {
13 try writer.writeByte(c);
14 bytes_written += 1;
15 } else {
16 const utf8_len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch unreachable;
17 try writer.writeAll(utf8_buf[0..utf8_len]);
18 bytes_written += utf8_len;
19 }
20 }
21}
22
23/// Returns the number of code units written to the writer
24pub fn windows1252ToUtf16AllocZ(allocator: std.mem.Allocator, win1252_str: []const u8) ![:0]u16 {
25 // Guaranteed to need exactly the same number of code units as Windows-1252 bytes
26 var utf16_slice = try allocator.allocSentinel(u16, win1252_str.len, 0);
27 errdefer allocator.free(utf16_slice);
28 for (win1252_str, 0..) |c, i| {
29 utf16_slice[i] = toCodepoint(c);
30 }
31 return utf16_slice;
32}
33
34/// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt
35pub fn toCodepoint(c: u8) u16 {
36 return switch (c) {
37 0x80 => 0x20ac, // Euro Sign
38 0x82 => 0x201a, // Single Low-9 Quotation Mark
39 0x83 => 0x0192, // Latin Small Letter F With Hook
40 0x84 => 0x201e, // Double Low-9 Quotation Mark
41 0x85 => 0x2026, // Horizontal Ellipsis
42 0x86 => 0x2020, // Dagger
43 0x87 => 0x2021, // Double Dagger
44 0x88 => 0x02c6, // Modifier Letter Circumflex Accent
45 0x89 => 0x2030, // Per Mille Sign
46 0x8a => 0x0160, // Latin Capital Letter S With Caron
47 0x8b => 0x2039, // Single Left-Pointing Angle Quotation Mark
48 0x8c => 0x0152, // Latin Capital Ligature Oe
49 0x8e => 0x017d, // Latin Capital Letter Z With Caron
50 0x91 => 0x2018, // Left Single Quotation Mark
51 0x92 => 0x2019, // Right Single Quotation Mark
52 0x93 => 0x201c, // Left Double Quotation Mark
53 0x94 => 0x201d, // Right Double Quotation Mark
54 0x95 => 0x2022, // Bullet
55 0x96 => 0x2013, // En Dash
56 0x97 => 0x2014, // Em Dash
57 0x98 => 0x02dc, // Small Tilde
58 0x99 => 0x2122, // Trade Mark Sign
59 0x9a => 0x0161, // Latin Small Letter S With Caron
60 0x9b => 0x203a, // Single Right-Pointing Angle Quotation Mark
61 0x9c => 0x0153, // Latin Small Ligature Oe
62 0x9e => 0x017e, // Latin Small Letter Z With Caron
63 0x9f => 0x0178, // Latin Capital Letter Y With Diaeresis
64 else => c,
65 };
66}
67
68/// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt
69/// Plus some mappings found empirically by iterating all codepoints:
70/// 0x2007 => 0xA0, // Figure Space
71/// 0x2008 => ' ', // Punctuation Space
72/// 0x2009 => ' ', // Thin Space
73/// 0x200A => ' ', // Hair Space
74/// 0x2012 => '-', // Figure Dash
75/// 0x2015 => '-', // Horizontal Bar
76/// 0x201B => '\'', // Single High-reversed-9 Quotation Mark
77/// 0x201F => '"', // Double High-reversed-9 Quotation Mark
78/// 0x202F => 0xA0, // Narrow No-Break Space
79/// 0x2033 => '"', // Double Prime
80/// 0x2036 => '"', // Reversed Double Prime
81pub fn bestFitFromCodepoint(codepoint: u21) ?u8 {
82 return switch (codepoint) {
83 0x00...0x7F,
84 0x81,
85 0x8D,
86 0x8F,
87 0x90,
88 0x9D,
89 0xA0...0xFF,
90 => @intCast(codepoint),
91 0x0100 => 0x41, // Latin Capital Letter A With Macron
92 0x0101 => 0x61, // Latin Small Letter A With Macron
93 0x0102 => 0x41, // Latin Capital Letter A With Breve
94 0x0103 => 0x61, // Latin Small Letter A With Breve
95 0x0104 => 0x41, // Latin Capital Letter A With Ogonek
96 0x0105 => 0x61, // Latin Small Letter A With Ogonek
97 0x0106 => 0x43, // Latin Capital Letter C With Acute
98 0x0107 => 0x63, // Latin Small Letter C With Acute
99 0x0108 => 0x43, // Latin Capital Letter C With Circumflex
100 0x0109 => 0x63, // Latin Small Letter C With Circumflex
101 0x010a => 0x43, // Latin Capital Letter C With Dot Above
102 0x010b => 0x63, // Latin Small Letter C With Dot Above
103 0x010c => 0x43, // Latin Capital Letter C With Caron
104 0x010d => 0x63, // Latin Small Letter C With Caron
105 0x010e => 0x44, // Latin Capital Letter D With Caron
106 0x010f => 0x64, // Latin Small Letter D With Caron
107 0x0110 => 0xd0, // Latin Capital Letter D With Stroke
108 0x0111 => 0x64, // Latin Small Letter D With Stroke
109 0x0112 => 0x45, // Latin Capital Letter E With Macron
110 0x0113 => 0x65, // Latin Small Letter E With Macron
111 0x0114 => 0x45, // Latin Capital Letter E With Breve
112 0x0115 => 0x65, // Latin Small Letter E With Breve
113 0x0116 => 0x45, // Latin Capital Letter E With Dot Above
114 0x0117 => 0x65, // Latin Small Letter E With Dot Above
115 0x0118 => 0x45, // Latin Capital Letter E With Ogonek
116 0x0119 => 0x65, // Latin Small Letter E With Ogonek
117 0x011a => 0x45, // Latin Capital Letter E With Caron
118 0x011b => 0x65, // Latin Small Letter E With Caron
119 0x011c => 0x47, // Latin Capital Letter G With Circumflex
120 0x011d => 0x67, // Latin Small Letter G With Circumflex
121 0x011e => 0x47, // Latin Capital Letter G With Breve
122 0x011f => 0x67, // Latin Small Letter G With Breve
123 0x0120 => 0x47, // Latin Capital Letter G With Dot Above
124 0x0121 => 0x67, // Latin Small Letter G With Dot Above
125 0x0122 => 0x47, // Latin Capital Letter G With Cedilla
126 0x0123 => 0x67, // Latin Small Letter G With Cedilla
127 0x0124 => 0x48, // Latin Capital Letter H With Circumflex
128 0x0125 => 0x68, // Latin Small Letter H With Circumflex
129 0x0126 => 0x48, // Latin Capital Letter H With Stroke
130 0x0127 => 0x68, // Latin Small Letter H With Stroke
131 0x0128 => 0x49, // Latin Capital Letter I With Tilde
132 0x0129 => 0x69, // Latin Small Letter I With Tilde
133 0x012a => 0x49, // Latin Capital Letter I With Macron
134 0x012b => 0x69, // Latin Small Letter I With Macron
135 0x012c => 0x49, // Latin Capital Letter I With Breve
136 0x012d => 0x69, // Latin Small Letter I With Breve
137 0x012e => 0x49, // Latin Capital Letter I With Ogonek
138 0x012f => 0x69, // Latin Small Letter I With Ogonek
139 0x0130 => 0x49, // Latin Capital Letter I With Dot Above
140 0x0131 => 0x69, // Latin Small Letter Dotless I
141 0x0134 => 0x4a, // Latin Capital Letter J With Circumflex
142 0x0135 => 0x6a, // Latin Small Letter J With Circumflex
143 0x0136 => 0x4b, // Latin Capital Letter K With Cedilla
144 0x0137 => 0x6b, // Latin Small Letter K With Cedilla
145 0x0139 => 0x4c, // Latin Capital Letter L With Acute
146 0x013a => 0x6c, // Latin Small Letter L With Acute
147 0x013b => 0x4c, // Latin Capital Letter L With Cedilla
148 0x013c => 0x6c, // Latin Small Letter L With Cedilla
149 0x013d => 0x4c, // Latin Capital Letter L With Caron
150 0x013e => 0x6c, // Latin Small Letter L With Caron
151 0x0141 => 0x4c, // Latin Capital Letter L With Stroke
152 0x0142 => 0x6c, // Latin Small Letter L With Stroke
153 0x0143 => 0x4e, // Latin Capital Letter N With Acute
154 0x0144 => 0x6e, // Latin Small Letter N With Acute
155 0x0145 => 0x4e, // Latin Capital Letter N With Cedilla
156 0x0146 => 0x6e, // Latin Small Letter N With Cedilla
157 0x0147 => 0x4e, // Latin Capital Letter N With Caron
158 0x0148 => 0x6e, // Latin Small Letter N With Caron
159 0x014c => 0x4f, // Latin Capital Letter O With Macron
160 0x014d => 0x6f, // Latin Small Letter O With Macron
161 0x014e => 0x4f, // Latin Capital Letter O With Breve
162 0x014f => 0x6f, // Latin Small Letter O With Breve
163 0x0150 => 0x4f, // Latin Capital Letter O With Double Acute
164 0x0151 => 0x6f, // Latin Small Letter O With Double Acute
165 0x0152 => 0x8c, // Latin Capital Ligature Oe
166 0x0153 => 0x9c, // Latin Small Ligature Oe
167 0x0154 => 0x52, // Latin Capital Letter R With Acute
168 0x0155 => 0x72, // Latin Small Letter R With Acute
169 0x0156 => 0x52, // Latin Capital Letter R With Cedilla
170 0x0157 => 0x72, // Latin Small Letter R With Cedilla
171 0x0158 => 0x52, // Latin Capital Letter R With Caron
172 0x0159 => 0x72, // Latin Small Letter R With Caron
173 0x015a => 0x53, // Latin Capital Letter S With Acute
174 0x015b => 0x73, // Latin Small Letter S With Acute
175 0x015c => 0x53, // Latin Capital Letter S With Circumflex
176 0x015d => 0x73, // Latin Small Letter S With Circumflex
177 0x015e => 0x53, // Latin Capital Letter S With Cedilla
178 0x015f => 0x73, // Latin Small Letter S With Cedilla
179 0x0160 => 0x8a, // Latin Capital Letter S With Caron
180 0x0161 => 0x9a, // Latin Small Letter S With Caron
181 0x0162 => 0x54, // Latin Capital Letter T With Cedilla
182 0x0163 => 0x74, // Latin Small Letter T With Cedilla
183 0x0164 => 0x54, // Latin Capital Letter T With Caron
184 0x0165 => 0x74, // Latin Small Letter T With Caron
185 0x0166 => 0x54, // Latin Capital Letter T With Stroke
186 0x0167 => 0x74, // Latin Small Letter T With Stroke
187 0x0168 => 0x55, // Latin Capital Letter U With Tilde
188 0x0169 => 0x75, // Latin Small Letter U With Tilde
189 0x016a => 0x55, // Latin Capital Letter U With Macron
190 0x016b => 0x75, // Latin Small Letter U With Macron
191 0x016c => 0x55, // Latin Capital Letter U With Breve
192 0x016d => 0x75, // Latin Small Letter U With Breve
193 0x016e => 0x55, // Latin Capital Letter U With Ring Above
194 0x016f => 0x75, // Latin Small Letter U With Ring Above
195 0x0170 => 0x55, // Latin Capital Letter U With Double Acute
196 0x0171 => 0x75, // Latin Small Letter U With Double Acute
197 0x0172 => 0x55, // Latin Capital Letter U With Ogonek
198 0x0173 => 0x75, // Latin Small Letter U With Ogonek
199 0x0174 => 0x57, // Latin Capital Letter W With Circumflex
200 0x0175 => 0x77, // Latin Small Letter W With Circumflex
201 0x0176 => 0x59, // Latin Capital Letter Y With Circumflex
202 0x0177 => 0x79, // Latin Small Letter Y With Circumflex
203 0x0178 => 0x9f, // Latin Capital Letter Y With Diaeresis
204 0x0179 => 0x5a, // Latin Capital Letter Z With Acute
205 0x017a => 0x7a, // Latin Small Letter Z With Acute
206 0x017b => 0x5a, // Latin Capital Letter Z With Dot Above
207 0x017c => 0x7a, // Latin Small Letter Z With Dot Above
208 0x017d => 0x8e, // Latin Capital Letter Z With Caron
209 0x017e => 0x9e, // Latin Small Letter Z With Caron
210 0x0180 => 0x62, // Latin Small Letter B With Stroke
211 0x0189 => 0xd0, // Latin Capital Letter African D
212 0x0191 => 0x83, // Latin Capital Letter F With Hook
213 0x0192 => 0x83, // Latin Small Letter F With Hook
214 0x0197 => 0x49, // Latin Capital Letter I With Stroke
215 0x019a => 0x6c, // Latin Small Letter L With Bar
216 0x019f => 0x4f, // Latin Capital Letter O With Middle Tilde
217 0x01a0 => 0x4f, // Latin Capital Letter O With Horn
218 0x01a1 => 0x6f, // Latin Small Letter O With Horn
219 0x01ab => 0x74, // Latin Small Letter T With Palatal Hook
220 0x01ae => 0x54, // Latin Capital Letter T With Retroflex Hook
221 0x01af => 0x55, // Latin Capital Letter U With Horn
222 0x01b0 => 0x75, // Latin Small Letter U With Horn
223 0x01b6 => 0x7a, // Latin Small Letter Z With Stroke
224 0x01c0 => 0x7c, // Latin Letter Dental Click
225 0x01c3 => 0x21, // Latin Letter Retroflex Click
226 0x01cd => 0x41, // Latin Capital Letter A With Caron
227 0x01ce => 0x61, // Latin Small Letter A With Caron
228 0x01cf => 0x49, // Latin Capital Letter I With Caron
229 0x01d0 => 0x69, // Latin Small Letter I With Caron
230 0x01d1 => 0x4f, // Latin Capital Letter O With Caron
231 0x01d2 => 0x6f, // Latin Small Letter O With Caron
232 0x01d3 => 0x55, // Latin Capital Letter U With Caron
233 0x01d4 => 0x75, // Latin Small Letter U With Caron
234 0x01d5 => 0x55, // Latin Capital Letter U With Diaeresis And Macron
235 0x01d6 => 0x75, // Latin Small Letter U With Diaeresis And Macron
236 0x01d7 => 0x55, // Latin Capital Letter U With Diaeresis And Acute
237 0x01d8 => 0x75, // Latin Small Letter U With Diaeresis And Acute
238 0x01d9 => 0x55, // Latin Capital Letter U With Diaeresis And Caron
239 0x01da => 0x75, // Latin Small Letter U With Diaeresis And Caron
240 0x01db => 0x55, // Latin Capital Letter U With Diaeresis And Grave
241 0x01dc => 0x75, // Latin Small Letter U With Diaeresis And Grave
242 0x01de => 0x41, // Latin Capital Letter A With Diaeresis And Macron
243 0x01df => 0x61, // Latin Small Letter A With Diaeresis And Macron
244 0x01e4 => 0x47, // Latin Capital Letter G With Stroke
245 0x01e5 => 0x67, // Latin Small Letter G With Stroke
246 0x01e6 => 0x47, // Latin Capital Letter G With Caron
247 0x01e7 => 0x67, // Latin Small Letter G With Caron
248 0x01e8 => 0x4b, // Latin Capital Letter K With Caron
249 0x01e9 => 0x6b, // Latin Small Letter K With Caron
250 0x01ea => 0x4f, // Latin Capital Letter O With Ogonek
251 0x01eb => 0x6f, // Latin Small Letter O With Ogonek
252 0x01ec => 0x4f, // Latin Capital Letter O With Ogonek And Macron
253 0x01ed => 0x6f, // Latin Small Letter O With Ogonek And Macron
254 0x01f0 => 0x6a, // Latin Small Letter J With Caron
255 0x0261 => 0x67, // Latin Small Letter Script G
256 0x02b9 => 0x27, // Modifier Letter Prime
257 0x02ba => 0x22, // Modifier Letter Double Prime
258 0x02bc => 0x27, // Modifier Letter Apostrophe
259 0x02c4 => 0x5e, // Modifier Letter Up Arrowhead
260 0x02c6 => 0x88, // Modifier Letter Circumflex Accent
261 0x02c8 => 0x27, // Modifier Letter Vertical Line
262 0x02c9 => 0xaf, // Modifier Letter Macron
263 0x02ca => 0xb4, // Modifier Letter Acute Accent
264 0x02cb => 0x60, // Modifier Letter Grave Accent
265 0x02cd => 0x5f, // Modifier Letter Low Macron
266 0x02da => 0xb0, // Ring Above
267 0x02dc => 0x98, // Small Tilde
268 0x0300 => 0x60, // Combining Grave Accent
269 0x0301 => 0xb4, // Combining Acute Accent
270 0x0302 => 0x5e, // Combining Circumflex Accent
271 0x0303 => 0x7e, // Combining Tilde
272 0x0304 => 0xaf, // Combining Macron
273 0x0305 => 0xaf, // Combining Overline
274 0x0308 => 0xa8, // Combining Diaeresis
275 0x030a => 0xb0, // Combining Ring Above
276 0x030e => 0x22, // Combining Double Vertical Line Above
277 0x0327 => 0xb8, // Combining Cedilla
278 0x0331 => 0x5f, // Combining Macron Below
279 0x0332 => 0x5f, // Combining Low Line
280 0x037e => 0x3b, // Greek Question Mark
281 0x0393 => 0x47, // Greek Capital Letter Gamma
282 0x0398 => 0x54, // Greek Capital Letter Theta
283 0x03a3 => 0x53, // Greek Capital Letter Sigma
284 0x03a6 => 0x46, // Greek Capital Letter Phi
285 0x03a9 => 0x4f, // Greek Capital Letter Omega
286 0x03b1 => 0x61, // Greek Small Letter Alpha
287 0x03b2 => 0xdf, // Greek Small Letter Beta
288 0x03b4 => 0x64, // Greek Small Letter Delta
289 0x03b5 => 0x65, // Greek Small Letter Epsilon
290 0x03bc => 0xb5, // Greek Small Letter Mu
291 0x03c0 => 0x70, // Greek Small Letter Pi
292 0x03c3 => 0x73, // Greek Small Letter Sigma
293 0x03c4 => 0x74, // Greek Small Letter Tau
294 0x03c6 => 0x66, // Greek Small Letter Phi
295 0x04bb => 0x68, // Cyrillic Small Letter Shha
296 0x0589 => 0x3a, // Armenian Full Stop
297 0x066a => 0x25, // Arabic Percent Sign
298 0x2000 => 0x20, // En Quad
299 0x2001 => 0x20, // Em Quad
300 0x2002 => 0x20, // En Space
301 0x2003 => 0x20, // Em Space
302 0x2004 => 0x20, // Three-Per-Em Space
303 0x2005 => 0x20, // Four-Per-Em Space
304 0x2006 => 0x20, // Six-Per-Em Space
305 0x2010 => 0x2d, // Hyphen
306 0x2011 => 0x2d, // Non-Breaking Hyphen
307 0x2013 => 0x96, // En Dash
308 0x2014 => 0x97, // Em Dash
309 0x2017 => 0x3d, // Double Low Line
310 0x2018 => 0x91, // Left Single Quotation Mark
311 0x2019 => 0x92, // Right Single Quotation Mark
312 0x201a => 0x82, // Single Low-9 Quotation Mark
313 0x201c => 0x93, // Left Double Quotation Mark
314 0x201d => 0x94, // Right Double Quotation Mark
315 0x201e => 0x84, // Double Low-9 Quotation Mark
316 0x2020 => 0x86, // Dagger
317 0x2021 => 0x87, // Double Dagger
318 0x2022 => 0x95, // Bullet
319 0x2024 => 0xb7, // One Dot Leader
320 0x2026 => 0x85, // Horizontal Ellipsis
321 0x2030 => 0x89, // Per Mille Sign
322 0x2032 => 0x27, // Prime
323 0x2035 => 0x60, // Reversed Prime
324 0x2039 => 0x8b, // Single Left-Pointing Angle Quotation Mark
325 0x203a => 0x9b, // Single Right-Pointing Angle Quotation Mark
326 0x2044 => 0x2f, // Fraction Slash
327 0x2070 => 0xb0, // Superscript Zero
328 0x2074 => 0x34, // Superscript Four
329 0x2075 => 0x35, // Superscript Five
330 0x2076 => 0x36, // Superscript Six
331 0x2077 => 0x37, // Superscript Seven
332 0x2078 => 0x38, // Superscript Eight
333 0x207f => 0x6e, // Superscript Latin Small Letter N
334 0x2080 => 0x30, // Subscript Zero
335 0x2081 => 0x31, // Subscript One
336 0x2082 => 0x32, // Subscript Two
337 0x2083 => 0x33, // Subscript Three
338 0x2084 => 0x34, // Subscript Four
339 0x2085 => 0x35, // Subscript Five
340 0x2086 => 0x36, // Subscript Six
341 0x2087 => 0x37, // Subscript Seven
342 0x2088 => 0x38, // Subscript Eight
343 0x2089 => 0x39, // Subscript Nine
344 0x20ac => 0x80, // Euro Sign
345 0x20a1 => 0xa2, // Colon Sign
346 0x20a4 => 0xa3, // Lira Sign
347 0x20a7 => 0x50, // Peseta Sign
348 0x2102 => 0x43, // Double-Struck Capital C
349 0x2107 => 0x45, // Euler Constant
350 0x210a => 0x67, // Script Small G
351 0x210b => 0x48, // Script Capital H
352 0x210c => 0x48, // Black-Letter Capital H
353 0x210d => 0x48, // Double-Struck Capital H
354 0x210e => 0x68, // Planck Constant
355 0x2110 => 0x49, // Script Capital I
356 0x2111 => 0x49, // Black-Letter Capital I
357 0x2112 => 0x4c, // Script Capital L
358 0x2113 => 0x6c, // Script Small L
359 0x2115 => 0x4e, // Double-Struck Capital N
360 0x2118 => 0x50, // Script Capital P
361 0x2119 => 0x50, // Double-Struck Capital P
362 0x211a => 0x51, // Double-Struck Capital Q
363 0x211b => 0x52, // Script Capital R
364 0x211c => 0x52, // Black-Letter Capital R
365 0x211d => 0x52, // Double-Struck Capital R
366 0x2122 => 0x99, // Trade Mark Sign
367 0x2124 => 0x5a, // Double-Struck Capital Z
368 0x2128 => 0x5a, // Black-Letter Capital Z
369 0x212a => 0x4b, // Kelvin Sign
370 0x212b => 0xc5, // Angstrom Sign
371 0x212c => 0x42, // Script Capital B
372 0x212d => 0x43, // Black-Letter Capital C
373 0x212e => 0x65, // Estimated Symbol
374 0x212f => 0x65, // Script Small E
375 0x2130 => 0x45, // Script Capital E
376 0x2131 => 0x46, // Script Capital F
377 0x2133 => 0x4d, // Script Capital M
378 0x2134 => 0x6f, // Script Small O
379 0x2205 => 0xd8, // Empty Set
380 0x2212 => 0x2d, // Minus Sign
381 0x2213 => 0xb1, // Minus-Or-Plus Sign
382 0x2215 => 0x2f, // Division Slash
383 0x2216 => 0x5c, // Set Minus
384 0x2217 => 0x2a, // Asterisk Operator
385 0x2218 => 0xb0, // Ring Operator
386 0x2219 => 0xb7, // Bullet Operator
387 0x221a => 0x76, // Square Root
388 0x221e => 0x38, // Infinity
389 0x2223 => 0x7c, // Divides
390 0x2229 => 0x6e, // Intersection
391 0x2236 => 0x3a, // Ratio
392 0x223c => 0x7e, // Tilde Operator
393 0x2248 => 0x98, // Almost Equal To
394 0x2261 => 0x3d, // Identical To
395 0x2264 => 0x3d, // Less-Than Or Equal To
396 0x2265 => 0x3d, // Greater-Than Or Equal To
397 0x226a => 0xab, // Much Less-Than
398 0x226b => 0xbb, // Much Greater-Than
399 0x22c5 => 0xb7, // Dot Operator
400 0x2302 => 0xa6, // House
401 0x2303 => 0x5e, // Up Arrowhead
402 0x2310 => 0xac, // Reversed Not Sign
403 0x2320 => 0x28, // Top Half Integral
404 0x2321 => 0x29, // Bottom Half Integral
405 0x2329 => 0x3c, // Left-Pointing Angle Bracket
406 0x232a => 0x3e, // Right-Pointing Angle Bracket
407 0x2500 => 0x2d, // Box Drawings Light Horizontal
408 0x2502 => 0xa6, // Box Drawings Light Vertical
409 0x250c => 0x2b, // Box Drawings Light Down And Right
410 0x2510 => 0x2b, // Box Drawings Light Down And Left
411 0x2514 => 0x2b, // Box Drawings Light Up And Right
412 0x2518 => 0x2b, // Box Drawings Light Up And Left
413 0x251c => 0x2b, // Box Drawings Light Vertical And Right
414 0x2524 => 0xa6, // Box Drawings Light Vertical And Left
415 0x252c => 0x2d, // Box Drawings Light Down And Horizontal
416 0x2534 => 0x2d, // Box Drawings Light Up And Horizontal
417 0x253c => 0x2b, // Box Drawings Light Vertical And Horizontal
418 0x2550 => 0x2d, // Box Drawings Double Horizontal
419 0x2551 => 0xa6, // Box Drawings Double Vertical
420 0x2552 => 0x2b, // Box Drawings Down Single And Right Double
421 0x2553 => 0x2b, // Box Drawings Down Double And Right Single
422 0x2554 => 0x2b, // Box Drawings Double Down And Right
423 0x2555 => 0x2b, // Box Drawings Down Single And Left Double
424 0x2556 => 0x2b, // Box Drawings Down Double And Left Single
425 0x2557 => 0x2b, // Box Drawings Double Down And Left
426 0x2558 => 0x2b, // Box Drawings Up Single And Right Double
427 0x2559 => 0x2b, // Box Drawings Up Double And Right Single
428 0x255a => 0x2b, // Box Drawings Double Up And Right
429 0x255b => 0x2b, // Box Drawings Up Single And Left Double
430 0x255c => 0x2b, // Box Drawings Up Double And Left Single
431 0x255d => 0x2b, // Box Drawings Double Up And Left
432 0x255e => 0xa6, // Box Drawings Vertical Single And Right Double
433 0x255f => 0xa6, // Box Drawings Vertical Double And Right Single
434 0x2560 => 0xa6, // Box Drawings Double Vertical And Right
435 0x2561 => 0xa6, // Box Drawings Vertical Single And Left Double
436 0x2562 => 0xa6, // Box Drawings Vertical Double And Left Single
437 0x2563 => 0xa6, // Box Drawings Double Vertical And Left
438 0x2564 => 0x2d, // Box Drawings Down Single And Horizontal Double
439 0x2565 => 0x2d, // Box Drawings Down Double And Horizontal Single
440 0x2566 => 0x2d, // Box Drawings Double Down And Horizontal
441 0x2567 => 0x2d, // Box Drawings Up Single And Horizontal Double
442 0x2568 => 0x2d, // Box Drawings Up Double And Horizontal Single
443 0x2569 => 0x2d, // Box Drawings Double Up And Horizontal
444 0x256a => 0x2b, // Box Drawings Vertical Single And Horizontal Double
445 0x256b => 0x2b, // Box Drawings Vertical Double And Horizontal Single
446 0x256c => 0x2b, // Box Drawings Double Vertical And Horizontal
447 0x2580 => 0xaf, // Upper Half Block
448 0x2584 => 0x5f, // Lower Half Block
449 0x2588 => 0xa6, // Full Block
450 0x258c => 0xa6, // Left Half Block
451 0x2590 => 0xa6, // Right Half Block
452 0x2591 => 0xa6, // Light Shade
453 0x2592 => 0xa6, // Medium Shade
454 0x2593 => 0xa6, // Dark Shade
455 0x25a0 => 0xa6, // Black Square
456 0x263c => 0xa4, // White Sun With Rays
457 0x2758 => 0x7c, // Light Vertical Bar
458 0x3000 => 0x20, // Ideographic Space
459 0x3008 => 0x3c, // Left Angle Bracket
460 0x3009 => 0x3e, // Right Angle Bracket
461 0x300a => 0xab, // Left Double Angle Bracket
462 0x300b => 0xbb, // Right Double Angle Bracket
463 0x301a => 0x5b, // Left White Square Bracket
464 0x301b => 0x5d, // Right White Square Bracket
465 0x30fb => 0xb7, // Katakana Middle Dot
466 0xff01 => 0x21, // Fullwidth Exclamation Mark
467 0xff02 => 0x22, // Fullwidth Quotation Mark
468 0xff03 => 0x23, // Fullwidth Number Sign
469 0xff04 => 0x24, // Fullwidth Dollar Sign
470 0xff05 => 0x25, // Fullwidth Percent Sign
471 0xff06 => 0x26, // Fullwidth Ampersand
472 0xff07 => 0x27, // Fullwidth Apostrophe
473 0xff08 => 0x28, // Fullwidth Left Parenthesis
474 0xff09 => 0x29, // Fullwidth Right Parenthesis
475 0xff0a => 0x2a, // Fullwidth Asterisk
476 0xff0b => 0x2b, // Fullwidth Plus Sign
477 0xff0c => 0x2c, // Fullwidth Comma
478 0xff0d => 0x2d, // Fullwidth Hyphen-Minus
479 0xff0e => 0x2e, // Fullwidth Full Stop
480 0xff0f => 0x2f, // Fullwidth Solidus
481 0xff10 => 0x30, // Fullwidth Digit Zero
482 0xff11 => 0x31, // Fullwidth Digit One
483 0xff12 => 0x32, // Fullwidth Digit Two
484 0xff13 => 0x33, // Fullwidth Digit Three
485 0xff14 => 0x34, // Fullwidth Digit Four
486 0xff15 => 0x35, // Fullwidth Digit Five
487 0xff16 => 0x36, // Fullwidth Digit Six
488 0xff17 => 0x37, // Fullwidth Digit Seven
489 0xff18 => 0x38, // Fullwidth Digit Eight
490 0xff19 => 0x39, // Fullwidth Digit Nine
491 0xff1a => 0x3a, // Fullwidth Colon
492 0xff1b => 0x3b, // Fullwidth Semicolon
493 0xff1c => 0x3c, // Fullwidth Less-Than Sign
494 0xff1d => 0x3d, // Fullwidth Equals Sign
495 0xff1e => 0x3e, // Fullwidth Greater-Than Sign
496 0xff1f => 0x3f, // Fullwidth Question Mark
497 0xff20 => 0x40, // Fullwidth Commercial At
498 0xff21 => 0x41, // Fullwidth Latin Capital Letter A
499 0xff22 => 0x42, // Fullwidth Latin Capital Letter B
500 0xff23 => 0x43, // Fullwidth Latin Capital Letter C
501 0xff24 => 0x44, // Fullwidth Latin Capital Letter D
502 0xff25 => 0x45, // Fullwidth Latin Capital Letter E
503 0xff26 => 0x46, // Fullwidth Latin Capital Letter F
504 0xff27 => 0x47, // Fullwidth Latin Capital Letter G
505 0xff28 => 0x48, // Fullwidth Latin Capital Letter H
506 0xff29 => 0x49, // Fullwidth Latin Capital Letter I
507 0xff2a => 0x4a, // Fullwidth Latin Capital Letter J
508 0xff2b => 0x4b, // Fullwidth Latin Capital Letter K
509 0xff2c => 0x4c, // Fullwidth Latin Capital Letter L
510 0xff2d => 0x4d, // Fullwidth Latin Capital Letter M
511 0xff2e => 0x4e, // Fullwidth Latin Capital Letter N
512 0xff2f => 0x4f, // Fullwidth Latin Capital Letter O
513 0xff30 => 0x50, // Fullwidth Latin Capital Letter P
514 0xff31 => 0x51, // Fullwidth Latin Capital Letter Q
515 0xff32 => 0x52, // Fullwidth Latin Capital Letter R
516 0xff33 => 0x53, // Fullwidth Latin Capital Letter S
517 0xff34 => 0x54, // Fullwidth Latin Capital Letter T
518 0xff35 => 0x55, // Fullwidth Latin Capital Letter U
519 0xff36 => 0x56, // Fullwidth Latin Capital Letter V
520 0xff37 => 0x57, // Fullwidth Latin Capital Letter W
521 0xff38 => 0x58, // Fullwidth Latin Capital Letter X
522 0xff39 => 0x59, // Fullwidth Latin Capital Letter Y
523 0xff3a => 0x5a, // Fullwidth Latin Capital Letter Z
524 0xff3b => 0x5b, // Fullwidth Left Square Bracket
525 0xff3c => 0x5c, // Fullwidth Reverse Solidus
526 0xff3d => 0x5d, // Fullwidth Right Square Bracket
527 0xff3e => 0x5e, // Fullwidth Circumflex Accent
528 0xff3f => 0x5f, // Fullwidth Low Line
529 0xff40 => 0x60, // Fullwidth Grave Accent
530 0xff41 => 0x61, // Fullwidth Latin Small Letter A
531 0xff42 => 0x62, // Fullwidth Latin Small Letter B
532 0xff43 => 0x63, // Fullwidth Latin Small Letter C
533 0xff44 => 0x64, // Fullwidth Latin Small Letter D
534 0xff45 => 0x65, // Fullwidth Latin Small Letter E
535 0xff46 => 0x66, // Fullwidth Latin Small Letter F
536 0xff47 => 0x67, // Fullwidth Latin Small Letter G
537 0xff48 => 0x68, // Fullwidth Latin Small Letter H
538 0xff49 => 0x69, // Fullwidth Latin Small Letter I
539 0xff4a => 0x6a, // Fullwidth Latin Small Letter J
540 0xff4b => 0x6b, // Fullwidth Latin Small Letter K
541 0xff4c => 0x6c, // Fullwidth Latin Small Letter L
542 0xff4d => 0x6d, // Fullwidth Latin Small Letter M
543 0xff4e => 0x6e, // Fullwidth Latin Small Letter N
544 0xff4f => 0x6f, // Fullwidth Latin Small Letter O
545 0xff50 => 0x70, // Fullwidth Latin Small Letter P
546 0xff51 => 0x71, // Fullwidth Latin Small Letter Q
547 0xff52 => 0x72, // Fullwidth Latin Small Letter R
548 0xff53 => 0x73, // Fullwidth Latin Small Letter S
549 0xff54 => 0x74, // Fullwidth Latin Small Letter T
550 0xff55 => 0x75, // Fullwidth Latin Small Letter U
551 0xff56 => 0x76, // Fullwidth Latin Small Letter V
552 0xff57 => 0x77, // Fullwidth Latin Small Letter W
553 0xff58 => 0x78, // Fullwidth Latin Small Letter X
554 0xff59 => 0x79, // Fullwidth Latin Small Letter Y
555 0xff5a => 0x7a, // Fullwidth Latin Small Letter Z
556 0xff5b => 0x7b, // Fullwidth Left Curly Bracket
557 0xff5c => 0x7c, // Fullwidth Vertical Line
558 0xff5d => 0x7d, // Fullwidth Right Curly Bracket
559 0xff5e => 0x7e, // Fullwidth Tilde
560 // Not in the best fit mapping, but RC uses these mappings too
561 0x2007 => 0xA0, // Figure Space
562 0x2008 => ' ', // Punctuation Space
563 0x2009 => ' ', // Thin Space
564 0x200A => ' ', // Hair Space
565 0x2012 => '-', // Figure Dash
566 0x2015 => '-', // Horizontal Bar
567 0x201B => '\'', // Single High-reversed-9 Quotation Mark
568 0x201F => '"', // Double High-reversed-9 Quotation Mark
569 0x202F => 0xA0, // Narrow No-Break Space
570 0x2033 => '"', // Double Prime
571 0x2036 => '"', // Reversed Double Prime
572 else => null,
573 };
574}
575
576test "windows-1252 to utf8" {
577 var buf = std.ArrayList(u8).init(std.testing.allocator);
578 defer buf.deinit();
579
580 const input_windows1252 = "\x81pqrstuvwxyz{|}~\x80\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8e\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9e\x9f\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff";
581 const expected_utf8 = "\xc2\x81pqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ";
582
583 var fbs = std.io.fixedBufferStream(input_windows1252);
584 const bytes_written = try windows1252ToUtf8Stream(buf.writer(), fbs.reader());
585
586 try std.testing.expectEqualStrings(expected_utf8, buf.items);
587 try std.testing.expectEqual(expected_utf8.len, bytes_written);
588}
test/standalone.zig+4
......@@ -194,6 +194,10 @@ pub const build_cases = [_]BuildCase{
194194 .build_root = "test/standalone/load_dynamic_library",
195195 .import = @import("standalone/load_dynamic_library/build.zig"),
196196 },
197 .{
198 .build_root = "test/standalone/windows_resources",
199 .import = @import("standalone/windows_resources/build.zig"),
200 },
197201 .{
198202 .build_root = "test/standalone/windows_spawn",
199203 .import = @import("standalone/windows_spawn/build.zig"),
test/standalone/windows_resources/build.zig created+40
......@@ -0,0 +1,40 @@
1const std = @import("std");
2
3pub fn build(b: *std.Build) void {
4 const test_step = b.step("test", "Test it");
5 b.default_step = test_step;
6
7 const native_target: std.zig.CrossTarget = .{};
8 const cross_target = .{
9 .cpu_arch = .x86_64,
10 .os_tag = .windows,
11 .abi = .gnu,
12 };
13
14 add(b, native_target, .any, test_step);
15 add(b, cross_target, .any, test_step);
16
17 add(b, native_target, .gnu, test_step);
18 add(b, cross_target, .gnu, test_step);
19}
20
21fn add(b: *std.Build, target: std.zig.CrossTarget, rc_includes: enum { any, gnu }, test_step: *std.Build.Step) void {
22 const exe = b.addExecutable(.{
23 .name = "zig_resource_test",
24 .root_source_file = .{ .path = "main.zig" },
25 .target = target,
26 .optimize = .Debug,
27 });
28 exe.addWin32ResourceFile(.{
29 .file = .{ .path = "res/zig.rc" },
30 .flags = &.{"/c65001"}, // UTF-8 code page
31 });
32 exe.rc_includes = switch (rc_includes) {
33 .any => .any,
34 .gnu => .gnu,
35 };
36
37 _ = exe.getEmittedBin();
38
39 test_step.dependOn(&exe.step);
40}
test/standalone/windows_resources/main.zig created+5
......@@ -0,0 +1,5 @@
1const std = @import("std");
2
3pub fn main() !void {
4 std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
5}
test/standalone/windows_resources/res/hello.bin created+1
......@@ -0,0 +1 @@
1abcdefg
\ No newline at end of file
test/standalone/windows_resources/res/sub/sub.rc created+1
......@@ -0,0 +1 @@
12 RCDATA hello.bin
test/standalone/windows_resources/res/zig.ico created
Binary files /dev/null and b/test/standalone/windows_resources/res/zig.ico differ
test/standalone/windows_resources/res/zig.rc created+40
......@@ -0,0 +1,40 @@
1#define ICO_ID 1
2
3// Nothing from windows.h is used in this .rc file,
4// but it's common to include it within a .rc file
5// so this makes sure that it can be found on
6// all platforms.
7#include "windows.h"
8
9ICO_ID ICON "zig.ico"
10
111 VERSIONINFO
12 FILEVERSION 1L,0,0,2
13 PRODUCTVERSION 1,0,0,1
14 FILEFLAGSMASK 0x3fL
15 FILEFLAGS 0x1L
16 FILEOS 0x4L
17 FILETYPE 0x1L
18 FILESUBTYPE 0x0L
19BEGIN
20 BLOCK "StringFileInfo"
21 BEGIN
22 BLOCK "040904e4"
23 BEGIN
24 VALUE "CompanyName", "Zig"
25 VALUE "FileDescription", "My cool zig program"
26 VALUE "FileVersion", "1.0.0.1"
27 VALUE "InternalName", "zig-ico.exe"
28 VALUE "LegalCopyright", "(c) no one"
29 VALUE "OriginalFilename", "zig-ico.exe"
30 VALUE "ProductName", "Zig but with an icon"
31 VALUE "ProductVersion", "1.0.0.1"
32 END
33 END
34 BLOCK "VarFileInfo"
35 BEGIN
36 VALUE "Translation", 0x409, 1252
37 END
38END
39
40#include "sub/sub.rc"