| author | |
| committer | |
| log | 3fc74135749e32ba106572ed85a2e253b89fb0c6 |
| tree | fdefb9fa7dc6bda28b9a94ce0c01f711fb06f1ed |
| parent | 867e61e10b905cdd1559ed9364ed28decb17d328 |
| parent | 572956ce240478ed6bdc5d98237e25fea0bab3e5 |
| signature |
Add a `.rc` -> `.res` compiler to the Zig compiler32 files changed, 17043 insertions(+), 13 deletions(-)
lib/std/Build/Step/Compile.zig+65| ... | @@ -90,6 +90,14 @@ is_linking_libc: bool, | ... | @@ -90,6 +90,14 @@ is_linking_libc: bool, |
| 90 | is_linking_libcpp: bool, | 90 | is_linking_libcpp: bool, |
| 91 | vcpkg_bin_path: ?[]const u8 = null, | 91 | vcpkg_bin_path: ?[]const u8 = null, |
| 92 | 92 | ||
| 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. | ||
| 99 | rc_includes: enum { any, msvc, gnu, none } = .any, | ||
| 100 | |||
| 93 | installed_path: ?[]const u8, | 101 | installed_path: ?[]const u8, |
| 94 | 102 | ||
| 95 | /// Base address for an executable image. | 103 | /// Base address for an executable image. |
| ... | @@ -221,6 +229,26 @@ pub const CSourceFile = struct { | ... | @@ -221,6 +229,26 @@ pub const CSourceFile = struct { |
| 221 | } | 229 | } |
| 222 | }; | 230 | }; |
| 223 | 231 | ||
| 232 | pub 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 | |||
| 224 | pub const LinkObject = union(enum) { | 252 | pub const LinkObject = union(enum) { |
| 225 | static_path: LazyPath, | 253 | static_path: LazyPath, |
| 226 | other_step: *Compile, | 254 | other_step: *Compile, |
| ... | @@ -228,6 +256,7 @@ pub const LinkObject = union(enum) { | ... | @@ -228,6 +256,7 @@ pub const LinkObject = union(enum) { |
| 228 | assembly_file: LazyPath, | 256 | assembly_file: LazyPath, |
| 229 | c_source_file: *CSourceFile, | 257 | c_source_file: *CSourceFile, |
| 230 | c_source_files: *CSourceFiles, | 258 | c_source_files: *CSourceFiles, |
| 259 | win32_resource_file: *RcSourceFile, | ||
| 231 | }; | 260 | }; |
| 232 | 261 | ||
| 233 | pub const SystemLib = struct { | 262 | pub const SystemLib = struct { |
| ... | @@ -910,6 +939,18 @@ pub fn addCSourceFile(self: *Compile, source: CSourceFile) void { | ... | @@ -910,6 +939,18 @@ pub fn addCSourceFile(self: *Compile, source: CSourceFile) void { |
| 910 | source.file.addStepDependencies(&self.step); | 939 | source.file.addStepDependencies(&self.step); |
| 911 | } | 940 | } |
| 912 | 941 | ||
| 942 | pub 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 | |||
| 913 | pub fn setVerboseLink(self: *Compile, value: bool) void { | 954 | pub fn setVerboseLink(self: *Compile, value: bool) void { |
| 914 | self.verbose_link = value; | 955 | self.verbose_link = value; |
| 915 | } | 956 | } |
| ... | @@ -1358,6 +1399,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { | ... | @@ -1358,6 +1399,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { |
| 1358 | try transitive_deps.add(self.link_objects.items); | 1399 | try transitive_deps.add(self.link_objects.items); |
| 1359 | 1400 | ||
| 1360 | var prev_has_cflags = false; | 1401 | var prev_has_cflags = false; |
| 1402 | var prev_has_rcflags = false; | ||
| 1361 | var prev_search_strategy: SystemLib.SearchStrategy = .paths_first; | 1403 | var prev_search_strategy: SystemLib.SearchStrategy = .paths_first; |
| 1362 | var prev_preferred_link_mode: std.builtin.LinkMode = .Dynamic; | 1404 | var prev_preferred_link_mode: std.builtin.LinkMode = .Dynamic; |
| 1363 | 1405 | ||
| ... | @@ -1500,6 +1542,24 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { | ... | @@ -1500,6 +1542,24 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { |
| 1500 | try zig_args.append(b.pathFromRoot(file)); | 1542 | try zig_args.append(b.pathFromRoot(file)); |
| 1501 | } | 1543 | } |
| 1502 | }, | 1544 | }, |
| 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 | }, | ||
| 1503 | } | 1563 | } |
| 1504 | } | 1564 | } |
| 1505 | 1565 | ||
| ... | @@ -1897,6 +1957,11 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { | ... | @@ -1897,6 +1957,11 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { |
| 1897 | } | 1957 | } |
| 1898 | } | 1958 | } |
| 1899 | 1959 | ||
| 1960 | if (self.rc_includes != .any) { | ||
| 1961 | try zig_args.append("-rcincludes"); | ||
| 1962 | try zig_args.append(@tagName(self.rc_includes)); | ||
| 1963 | } | ||
| 1964 | |||
| 1900 | try addFlag(&zig_args, "valgrind", self.valgrind_support); | 1965 | try addFlag(&zig_args, "valgrind", self.valgrind_support); |
| 1901 | try addFlag(&zig_args, "each-lib-rpath", self.each_lib_rpath); | 1966 | try addFlag(&zig_args, "each-lib-rpath", self.each_lib_rpath); |
| 1902 | 1967 |
lib/std/zig/ErrorBundle.zig+20-2| ... | @@ -421,7 +421,7 @@ pub const Wip = struct { | ... | @@ -421,7 +421,7 @@ pub const Wip = struct { |
| 421 | _ = try addExtra(wip, rt); | 421 | _ = try addExtra(wip, rt); |
| 422 | } | 422 | } |
| 423 | 423 | ||
| 424 | pub fn addBundle(wip: *Wip, other: ErrorBundle) !void { | 424 | pub fn addBundleAsNotes(wip: *Wip, other: ErrorBundle) !void { |
| 425 | const gpa = wip.gpa; | 425 | const gpa = wip.gpa; |
| 426 | 426 | ||
| 427 | try wip.string_bytes.ensureUnusedCapacity(gpa, other.string_bytes.len); | 427 | try wip.string_bytes.ensureUnusedCapacity(gpa, other.string_bytes.len); |
| ... | @@ -436,6 +436,21 @@ pub const Wip = struct { | ... | @@ -436,6 +436,21 @@ pub const Wip = struct { |
| 436 | } | 436 | } |
| 437 | } | 437 | } |
| 438 | 438 | ||
| 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 | |||
| 439 | pub fn reserveNotes(wip: *Wip, notes_len: u32) !u32 { | 454 | pub fn reserveNotes(wip: *Wip, notes_len: u32) !u32 { |
| 440 | try wip.extra.ensureUnusedCapacity(wip.gpa, notes_len + | 455 | try wip.extra.ensureUnusedCapacity(wip.gpa, notes_len + |
| 441 | notes_len * @typeInfo(ErrorBundle.ErrorMessage).Struct.fields.len); | 456 | notes_len * @typeInfo(ErrorBundle.ErrorMessage).Struct.fields.len); |
| ... | @@ -474,7 +489,10 @@ pub const Wip = struct { | ... | @@ -474,7 +489,10 @@ pub const Wip = struct { |
| 474 | .span_start = other_sl.span_start, | 489 | .span_start = other_sl.span_start, |
| 475 | .span_main = other_sl.span_main, | 490 | .span_main = other_sl.span_main, |
| 476 | .span_end = other_sl.span_end, | 491 | .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, | ||
| 478 | .reference_trace_len = other_sl.reference_trace_len, | 496 | .reference_trace_len = other_sl.reference_trace_len, |
| 479 | }); | 497 | }); |
| 480 | 498 |
src/Compilation.zig+763-8| ... | @@ -39,6 +39,7 @@ const libtsan = @import("libtsan.zig"); | ... | @@ -39,6 +39,7 @@ const libtsan = @import("libtsan.zig"); |
| 39 | const Zir = @import("Zir.zig"); | 39 | const Zir = @import("Zir.zig"); |
| 40 | const Autodoc = @import("Autodoc.zig"); | 40 | const Autodoc = @import("Autodoc.zig"); |
| 41 | const Color = @import("main.zig").Color; | 41 | const Color = @import("main.zig").Color; |
| 42 | const resinator = @import("resinator.zig"); | ||
| 42 | 43 | ||
| 43 | /// General-purpose allocator. Used for both temporary and long-term storage. | 44 | /// General-purpose allocator. Used for both temporary and long-term storage. |
| 44 | gpa: Allocator, | 45 | gpa: Allocator, |
| ... | @@ -46,6 +47,7 @@ gpa: Allocator, | ... | @@ -46,6 +47,7 @@ gpa: Allocator, |
| 46 | arena_state: std.heap.ArenaAllocator.State, | 47 | arena_state: std.heap.ArenaAllocator.State, |
| 47 | bin_file: *link.File, | 48 | bin_file: *link.File, |
| 48 | c_object_table: std.AutoArrayHashMapUnmanaged(*CObject, void) = .{}, | 49 | c_object_table: std.AutoArrayHashMapUnmanaged(*CObject, void) = .{}, |
| 50 | win32_resource_table: std.AutoArrayHashMapUnmanaged(*Win32Resource, void) = .{}, | ||
| 49 | /// This is a pointer to a local variable inside `update()`. | 51 | /// This is a pointer to a local variable inside `update()`. |
| 50 | whole_cache_manifest: ?*Cache.Manifest = null, | 52 | whole_cache_manifest: ?*Cache.Manifest = null, |
| 51 | whole_cache_manifest_mutex: std.Thread.Mutex = .{}, | 53 | whole_cache_manifest_mutex: std.Thread.Mutex = .{}, |
| ... | @@ -60,6 +62,10 @@ anon_work_queue: std.fifo.LinearFifo(Job, .Dynamic), | ... | @@ -60,6 +62,10 @@ anon_work_queue: std.fifo.LinearFifo(Job, .Dynamic), |
| 60 | /// gets linked with the Compilation. | 62 | /// gets linked with the Compilation. |
| 61 | c_object_work_queue: std.fifo.LinearFifo(*CObject, .Dynamic), | 63 | c_object_work_queue: std.fifo.LinearFifo(*CObject, .Dynamic), |
| 62 | 64 | ||
| 65 | /// These jobs are to invoke the RC compiler to create a compiled resource file (.res), which | ||
| 66 | /// gets linked with the Compilation. | ||
| 67 | win32_resource_work_queue: std.fifo.LinearFifo(*Win32Resource, .Dynamic), | ||
| 68 | |||
| 63 | /// These jobs are to tokenize, parse, and astgen files, which may be outdated | 69 | /// These jobs are to tokenize, parse, and astgen files, which may be outdated |
| 64 | /// since the last compilation, as well as scan for `@import` and queue up | 70 | /// since the last compilation, as well as scan for `@import` and queue up |
| 65 | /// additional jobs corresponding to those new files. | 71 | /// additional jobs corresponding to those new files. |
| ... | @@ -73,6 +79,10 @@ embed_file_work_queue: std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic), | ... | @@ -73,6 +79,10 @@ embed_file_work_queue: std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic), |
| 73 | /// This data is accessed by multiple threads and is protected by `mutex`. | 79 | /// This data is accessed by multiple threads and is protected by `mutex`. |
| 74 | failed_c_objects: std.AutoArrayHashMapUnmanaged(*CObject, *CObject.ErrorMsg) = .{}, | 80 | failed_c_objects: std.AutoArrayHashMapUnmanaged(*CObject, *CObject.ErrorMsg) = .{}, |
| 75 | 81 | ||
| 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`. | ||
| 84 | failed_win32_resources: std.AutoArrayHashMapUnmanaged(*Win32Resource, ErrorBundle) = .{}, | ||
| 85 | |||
| 76 | /// Miscellaneous things that can fail. | 86 | /// Miscellaneous things that can fail. |
| 77 | misc_failures: std.AutoArrayHashMapUnmanaged(MiscTask, MiscError) = .{}, | 87 | misc_failures: std.AutoArrayHashMapUnmanaged(MiscTask, MiscError) = .{}, |
| 78 | 88 | ||
| ... | @@ -109,6 +119,7 @@ last_update_was_cache_hit: bool = false, | ... | @@ -109,6 +119,7 @@ last_update_was_cache_hit: bool = false, |
| 109 | 119 | ||
| 110 | c_source_files: []const CSourceFile, | 120 | c_source_files: []const CSourceFile, |
| 111 | clang_argv: []const []const u8, | 121 | clang_argv: []const []const u8, |
| 122 | rc_source_files: []const RcSourceFile, | ||
| 112 | cache_parent: *Cache, | 123 | cache_parent: *Cache, |
| 113 | /// Path to own executable for invoking `zig clang`. | 124 | /// Path to own executable for invoking `zig clang`. |
| 114 | self_exe_path: ?[]const u8, | 125 | self_exe_path: ?[]const u8, |
| ... | @@ -125,6 +136,7 @@ local_cache_directory: Directory, | ... | @@ -125,6 +136,7 @@ local_cache_directory: Directory, |
| 125 | global_cache_directory: Directory, | 136 | global_cache_directory: Directory, |
| 126 | libc_include_dir_list: []const []const u8, | 137 | libc_include_dir_list: []const []const u8, |
| 127 | libc_framework_dir_list: []const []const u8, | 138 | libc_framework_dir_list: []const []const u8, |
| 139 | rc_include_dir_list: []const []const u8, | ||
| 128 | thread_pool: *ThreadPool, | 140 | thread_pool: *ThreadPool, |
| 129 | 141 | ||
| 130 | /// Populated when we build the libc++ static library. A Job to build this is placed in the queue | 142 | /// 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 { | ... | @@ -225,6 +237,23 @@ pub const CSourceFile = struct { |
| 225 | ext: ?FileExt = null, | 237 | ext: ?FileExt = null, |
| 226 | }; | 238 | }; |
| 227 | 239 | ||
| 240 | /// For passing to resinator. | ||
| 241 | pub const RcSourceFile = struct { | ||
| 242 | src_path: []const u8, | ||
| 243 | extra_flags: []const []const u8 = &.{}, | ||
| 244 | }; | ||
| 245 | |||
| 246 | pub 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 | |||
| 228 | const Job = union(enum) { | 257 | const Job = union(enum) { |
| 229 | /// Write the constant value for a Decl to the output file. | 258 | /// Write the constant value for a Decl to the output file. |
| 230 | codegen_decl: Module.Decl.Index, | 259 | codegen_decl: Module.Decl.Index, |
| ... | @@ -326,6 +355,50 @@ pub const CObject = struct { | ... | @@ -326,6 +355,50 @@ pub const CObject = struct { |
| 326 | } | 355 | } |
| 327 | }; | 356 | }; |
| 328 | 357 | ||
| 358 | pub 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 | |||
| 329 | pub const MiscTask = enum { | 402 | pub const MiscTask = enum { |
| 330 | write_builtin_zig, | 403 | write_builtin_zig, |
| 331 | glibc_crt_file, | 404 | glibc_crt_file, |
| ... | @@ -505,6 +578,8 @@ pub const InitOptions = struct { | ... | @@ -505,6 +578,8 @@ pub const InitOptions = struct { |
| 505 | rpath_list: []const []const u8 = &[0][]const u8{}, | 578 | rpath_list: []const []const u8 = &[0][]const u8{}, |
| 506 | symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .{}, | 579 | symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .{}, |
| 507 | c_source_files: []const CSourceFile = &[0]CSourceFile{}, | 580 | c_source_files: []const CSourceFile = &[0]CSourceFile{}, |
| 581 | rc_source_files: []const RcSourceFile = &[0]RcSourceFile{}, | ||
| 582 | rc_includes: RcIncludes = .any, | ||
| 508 | link_objects: []LinkObject = &[0]LinkObject{}, | 583 | link_objects: []LinkObject = &[0]LinkObject{}, |
| 509 | framework_dirs: []const []const u8 = &[0][]const u8{}, | 584 | framework_dirs: []const []const u8 = &[0][]const u8{}, |
| 510 | frameworks: []const Framework = &.{}, | 585 | frameworks: []const Framework = &.{}, |
| ... | @@ -938,6 +1013,11 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { | ... | @@ -938,6 +1013,11 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { |
| 938 | options.libc_installation, | 1013 | options.libc_installation, |
| 939 | ); | 1014 | ); |
| 940 | 1015 | ||
| 1016 | const rc_dirs = try detectWin32ResourceIncludeDirs( | ||
| 1017 | arena, | ||
| 1018 | options, | ||
| 1019 | ); | ||
| 1020 | |||
| 941 | const sysroot = options.sysroot orelse libc_dirs.sysroot; | 1021 | const sysroot = options.sysroot orelse libc_dirs.sysroot; |
| 942 | 1022 | ||
| 943 | const must_pie = target_util.requiresPIE(options.target); | 1023 | const must_pie = target_util.requiresPIE(options.target); |
| ... | @@ -1591,16 +1671,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { | ... | @@ -1591,16 +1671,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { |
| 1591 | .work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa), | 1671 | .work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa), |
| 1592 | .anon_work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa), | 1672 | .anon_work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa), |
| 1593 | .c_object_work_queue = std.fifo.LinearFifo(*CObject, .Dynamic).init(gpa), | 1673 | .c_object_work_queue = std.fifo.LinearFifo(*CObject, .Dynamic).init(gpa), |
| 1674 | .win32_resource_work_queue = std.fifo.LinearFifo(*Win32Resource, .Dynamic).init(gpa), | ||
| 1594 | .astgen_work_queue = std.fifo.LinearFifo(*Module.File, .Dynamic).init(gpa), | 1675 | .astgen_work_queue = std.fifo.LinearFifo(*Module.File, .Dynamic).init(gpa), |
| 1595 | .embed_file_work_queue = std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic).init(gpa), | 1676 | .embed_file_work_queue = std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic).init(gpa), |
| 1596 | .keep_source_files_loaded = options.keep_source_files_loaded, | 1677 | .keep_source_files_loaded = options.keep_source_files_loaded, |
| 1597 | .use_clang = use_clang, | 1678 | .use_clang = use_clang, |
| 1598 | .clang_argv = options.clang_argv, | 1679 | .clang_argv = options.clang_argv, |
| 1599 | .c_source_files = options.c_source_files, | 1680 | .c_source_files = options.c_source_files, |
| 1681 | .rc_source_files = options.rc_source_files, | ||
| 1600 | .cache_parent = cache, | 1682 | .cache_parent = cache, |
| 1601 | .self_exe_path = options.self_exe_path, | 1683 | .self_exe_path = options.self_exe_path, |
| 1602 | .libc_include_dir_list = libc_dirs.libc_include_dir_list, | 1684 | .libc_include_dir_list = libc_dirs.libc_include_dir_list, |
| 1603 | .libc_framework_dir_list = libc_dirs.libc_framework_dir_list, | 1685 | .libc_framework_dir_list = libc_dirs.libc_framework_dir_list, |
| 1686 | .rc_include_dir_list = rc_dirs.libc_include_dir_list, | ||
| 1604 | .sanitize_c = sanitize_c, | 1687 | .sanitize_c = sanitize_c, |
| 1605 | .thread_pool = options.thread_pool, | 1688 | .thread_pool = options.thread_pool, |
| 1606 | .clang_passthrough_mode = options.clang_passthrough_mode, | 1689 | .clang_passthrough_mode = options.clang_passthrough_mode, |
| ... | @@ -1647,6 +1730,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { | ... | @@ -1647,6 +1730,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { |
| 1647 | comp.c_object_table.putAssumeCapacityNoClobber(c_object, {}); | 1730 | comp.c_object_table.putAssumeCapacityNoClobber(c_object, {}); |
| 1648 | } | 1731 | } |
| 1649 | 1732 | ||
| 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 | |||
| 1650 | const have_bin_emit = comp.bin_file.options.emit != null or comp.whole_bin_sub_path != null; | 1746 | const have_bin_emit = comp.bin_file.options.emit != null or comp.whole_bin_sub_path != null; |
| 1651 | 1747 | ||
| 1652 | if (have_bin_emit and !comp.bin_file.options.skip_linker_dependencies and target.ofmt != .c) { | 1748 | 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 { | ... | @@ -1804,6 +1900,7 @@ pub fn destroy(self: *Compilation) void { |
| 1804 | self.work_queue.deinit(); | 1900 | self.work_queue.deinit(); |
| 1805 | self.anon_work_queue.deinit(); | 1901 | self.anon_work_queue.deinit(); |
| 1806 | self.c_object_work_queue.deinit(); | 1902 | self.c_object_work_queue.deinit(); |
| 1903 | self.win32_resource_work_queue.deinit(); | ||
| 1807 | self.astgen_work_queue.deinit(); | 1904 | self.astgen_work_queue.deinit(); |
| 1808 | self.embed_file_work_queue.deinit(); | 1905 | self.embed_file_work_queue.deinit(); |
| 1809 | 1906 | ||
| ... | @@ -1852,6 +1949,16 @@ pub fn destroy(self: *Compilation) void { | ... | @@ -1852,6 +1949,16 @@ pub fn destroy(self: *Compilation) void { |
| 1852 | } | 1949 | } |
| 1853 | self.failed_c_objects.deinit(gpa); | 1950 | self.failed_c_objects.deinit(gpa); |
| 1854 | 1951 | ||
| 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 | |||
| 1855 | for (self.lld_errors.items) |*lld_error| { | 1962 | for (self.lld_errors.items) |*lld_error| { |
| 1856 | lld_error.deinit(gpa); | 1963 | lld_error.deinit(gpa); |
| 1857 | } | 1964 | } |
| ... | @@ -2014,6 +2121,13 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void | ... | @@ -2014,6 +2121,13 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void |
| 2014 | comp.c_object_work_queue.writeItemAssumeCapacity(key); | 2121 | comp.c_object_work_queue.writeItemAssumeCapacity(key); |
| 2015 | } | 2122 | } |
| 2016 | 2123 | ||
| 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 | |||
| 2017 | if (comp.bin_file.options.module) |module| { | 2131 | if (comp.bin_file.options.module) |module| { |
| 2018 | module.compile_log_text.shrinkAndFree(module.gpa, 0); | 2132 | module.compile_log_text.shrinkAndFree(module.gpa, 0); |
| 2019 | module.generation += 1; | 2133 | module.generation += 1; |
| ... | @@ -2336,6 +2450,13 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes | ... | @@ -2336,6 +2450,13 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes |
| 2336 | man.hash.addListOfBytes(key.src.extra_flags); | 2450 | man.hash.addListOfBytes(key.src.extra_flags); |
| 2337 | } | 2451 | } |
| 2338 | 2452 | ||
| 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 | |||
| 2339 | cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_asm); | 2460 | cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_asm); |
| 2340 | cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_ir); | 2461 | cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_ir); |
| 2341 | cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_bc); | 2462 | cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_bc); |
| ... | @@ -2571,8 +2692,14 @@ pub fn makeBinFileWritable(self: *Compilation) !void { | ... | @@ -2571,8 +2692,14 @@ pub fn makeBinFileWritable(self: *Compilation) !void { |
| 2571 | 2692 | ||
| 2572 | /// This function is temporally single-threaded. | 2693 | /// This function is temporally single-threaded. |
| 2573 | pub fn totalErrorCount(self: *Compilation) u32 { | 2694 | pub fn totalErrorCount(self: *Compilation) u32 { |
| 2574 | var total: usize = self.failed_c_objects.count() + self.misc_failures.count() + | 2695 | var total: usize = self.failed_c_objects.count() + |
| 2575 | @intFromBool(self.alloc_failure_occurred) + self.lld_errors.items.len; | 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 | } | ||
| 2576 | 2703 | ||
| 2577 | if (self.bin_file.options.module) |module| { | 2704 | if (self.bin_file.options.module) |module| { |
| 2578 | total += module.failed_exports.count(); | 2705 | total += module.failed_exports.count(); |
| ... | @@ -2664,6 +2791,13 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle { | ... | @@ -2664,6 +2791,13 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle { |
| 2664 | } | 2791 | } |
| 2665 | } | 2792 | } |
| 2666 | 2793 | ||
| 2794 | { | ||
| 2795 | var it = self.failed_win32_resources.iterator(); | ||
| 2796 | while (it.next()) |entry| { | ||
| 2797 | try bundle.addBundleAsRoots(entry.value_ptr.*); | ||
| 2798 | } | ||
| 2799 | } | ||
| 2800 | |||
| 2667 | for (self.lld_errors.items) |lld_error| { | 2801 | for (self.lld_errors.items) |lld_error| { |
| 2668 | const notes_len = @as(u32, @intCast(lld_error.context_lines.len)); | 2802 | const notes_len = @as(u32, @intCast(lld_error.context_lines.len)); |
| 2669 | 2803 | ||
| ... | @@ -2683,7 +2817,7 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle { | ... | @@ -2683,7 +2817,7 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle { |
| 2683 | .msg = try bundle.addString(value.msg), | 2817 | .msg = try bundle.addString(value.msg), |
| 2684 | .notes_len = if (value.children) |b| b.errorMessageCount() else 0, | 2818 | .notes_len = if (value.children) |b| b.errorMessageCount() else 0, |
| 2685 | }); | 2819 | }); |
| 2686 | if (value.children) |b| try bundle.addBundle(b); | 2820 | if (value.children) |b| try bundle.addBundleAsNotes(b); |
| 2687 | } | 2821 | } |
| 2688 | if (self.alloc_failure_occurred) { | 2822 | if (self.alloc_failure_occurred) { |
| 2689 | try bundle.addRootErrorMessage(.{ | 2823 | try bundle.addRootErrorMessage(.{ |
| ... | @@ -3082,6 +3216,9 @@ pub fn performAllTheWork( | ... | @@ -3082,6 +3216,9 @@ pub fn performAllTheWork( |
| 3082 | var c_obj_prog_node = main_progress_node.start("Compile C Objects", comp.c_source_files.len); | 3216 | var c_obj_prog_node = main_progress_node.start("Compile C Objects", comp.c_source_files.len); |
| 3083 | defer c_obj_prog_node.end(); | 3217 | defer c_obj_prog_node.end(); |
| 3084 | 3218 | ||
| 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 | |||
| 3085 | var embed_file_prog_node = main_progress_node.start("Detect @embedFile updates", comp.embed_file_work_queue.count); | 3222 | var embed_file_prog_node = main_progress_node.start("Detect @embedFile updates", comp.embed_file_work_queue.count); |
| 3086 | defer embed_file_prog_node.end(); | 3223 | defer embed_file_prog_node.end(); |
| 3087 | 3224 | ||
| ... | @@ -3130,6 +3267,13 @@ pub fn performAllTheWork( | ... | @@ -3130,6 +3267,13 @@ pub fn performAllTheWork( |
| 3130 | comp, c_object, &c_obj_prog_node, &comp.work_queue_wait_group, | 3267 | comp, c_object, &c_obj_prog_node, &comp.work_queue_wait_group, |
| 3131 | }); | 3268 | }); |
| 3132 | } | 3269 | } |
| 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 | } | ||
| 3133 | } | 3277 | } |
| 3134 | 3278 | ||
| 3135 | if (comp.bin_file.options.module) |mod| { | 3279 | if (comp.bin_file.options.module) |mod| { |
| ... | @@ -3659,6 +3803,14 @@ pub fn obtainCObjectCacheManifest(comp: *const Compilation) Cache.Manifest { | ... | @@ -3659,6 +3803,14 @@ pub fn obtainCObjectCacheManifest(comp: *const Compilation) Cache.Manifest { |
| 3659 | return man; | 3803 | return man; |
| 3660 | } | 3804 | } |
| 3661 | 3805 | ||
| 3806 | pub 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 | |||
| 3662 | test "cImport" { | 3814 | test "cImport" { |
| 3663 | _ = cImport; | 3815 | _ = cImport; |
| 3664 | } | 3816 | } |
| ... | @@ -3832,6 +3984,26 @@ fn workerUpdateCObject( | ... | @@ -3832,6 +3984,26 @@ fn workerUpdateCObject( |
| 3832 | }; | 3984 | }; |
| 3833 | } | 3985 | } |
| 3834 | 3986 | ||
| 3987 | fn 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 | |||
| 3835 | fn buildCompilerRtOneShot( | 4007 | fn buildCompilerRtOneShot( |
| 3836 | comp: *Compilation, | 4008 | comp: *Compilation, |
| 3837 | output_mode: std.builtin.OutputMode, | 4009 | output_mode: std.builtin.OutputMode, |
| ... | @@ -3877,6 +4049,18 @@ fn reportRetryableCObjectError( | ... | @@ -3877,6 +4049,18 @@ fn reportRetryableCObjectError( |
| 3877 | } | 4049 | } |
| 3878 | } | 4050 | } |
| 3879 | 4051 | ||
| 4052 | fn 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 | |||
| 3880 | fn reportRetryableAstGenError( | 4064 | fn reportRetryableAstGenError( |
| 3881 | comp: *Compilation, | 4065 | comp: *Compilation, |
| 3882 | src: AstGenSrc, | 4066 | src: AstGenSrc, |
| ... | @@ -4233,6 +4417,311 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P | ... | @@ -4233,6 +4417,311 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P |
| 4233 | }; | 4417 | }; |
| 4234 | } | 4418 | } |
| 4235 | 4419 | ||
| 4420 | fn 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 | |||
| 4236 | pub fn tmpFilePath(comp: *Compilation, ally: Allocator, suffix: []const u8) error{OutOfMemory}![]const u8 { | 4725 | pub fn tmpFilePath(comp: *Compilation, ally: Allocator, suffix: []const u8) error{OutOfMemory}![]const u8 { |
| 4237 | const s = std.fs.path.sep_str; | 4726 | const s = std.fs.path.sep_str; |
| 4238 | const rand_int = std.crypto.random.int(u64); | 4727 | const rand_int = std.crypto.random.int(u64); |
| ... | @@ -4350,7 +4839,7 @@ pub fn addCCArgs( | ... | @@ -4350,7 +4839,7 @@ pub fn addCCArgs( |
| 4350 | try argv.appendSlice(&[_][]const u8{ "-target", llvm_triple }); | 4839 | try argv.appendSlice(&[_][]const u8{ "-target", llvm_triple }); |
| 4351 | 4840 | ||
| 4352 | switch (ext) { | 4841 | switch (ext) { |
| 4353 | .c, .cpp, .m, .mm, .h, .cu => { | 4842 | .c, .cpp, .m, .mm, .h, .cu, .rc => { |
| 4354 | try argv.appendSlice(&[_][]const u8{ | 4843 | try argv.appendSlice(&[_][]const u8{ |
| 4355 | "-nostdinc", | 4844 | "-nostdinc", |
| 4356 | "-fno-spell-checking", | 4845 | "-fno-spell-checking", |
| ... | @@ -4378,9 +4867,16 @@ pub fn addCCArgs( | ... | @@ -4378,9 +4867,16 @@ pub fn addCCArgs( |
| 4378 | try argv.append("-isystem"); | 4867 | try argv.append("-isystem"); |
| 4379 | try argv.append(c_headers_dir); | 4868 | try argv.append(c_headers_dir); |
| 4380 | 4869 | ||
| 4381 | for (comp.libc_include_dir_list) |include_dir| { | 4870 | if (ext == .rc) { |
| 4382 | try argv.append("-isystem"); | 4871 | for (comp.rc_include_dir_list) |include_dir| { |
| 4383 | try argv.append(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 | } | ||
| 4384 | } | 4880 | } |
| 4385 | 4881 | ||
| 4386 | if (target.cpu.model.llvm_name) |llvm_name| { | 4882 | if (target.cpu.model.llvm_name) |llvm_name| { |
| ... | @@ -4692,6 +5188,253 @@ fn failCObjWithOwnedErrorMsg( | ... | @@ -4692,6 +5188,253 @@ fn failCObjWithOwnedErrorMsg( |
| 4692 | return error.AnalysisFail; | 5188 | return error.AnalysisFail; |
| 4693 | } | 5189 | } |
| 4694 | 5190 | ||
| 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. | ||
| 5200 | fn 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 | |||
| 5252 | fn 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 | |||
| 5272 | fn 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 | |||
| 5287 | fn 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 | |||
| 5342 | fn 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 | |||
| 5430 | fn 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 | |||
| 4695 | pub const FileExt = enum { | 5438 | pub const FileExt = enum { |
| 4696 | c, | 5439 | c, |
| 4697 | cpp, | 5440 | cpp, |
| ... | @@ -4708,6 +5451,7 @@ pub const FileExt = enum { | ... | @@ -4708,6 +5451,7 @@ pub const FileExt = enum { |
| 4708 | static_library, | 5451 | static_library, |
| 4709 | zig, | 5452 | zig, |
| 4710 | def, | 5453 | def, |
| 5454 | rc, | ||
| 4711 | res, | 5455 | res, |
| 4712 | unknown, | 5456 | unknown, |
| 4713 | 5457 | ||
| ... | @@ -4724,6 +5468,7 @@ pub const FileExt = enum { | ... | @@ -4724,6 +5468,7 @@ pub const FileExt = enum { |
| 4724 | .static_library, | 5468 | .static_library, |
| 4725 | .zig, | 5469 | .zig, |
| 4726 | .def, | 5470 | .def, |
| 5471 | .rc, | ||
| 4727 | .res, | 5472 | .res, |
| 4728 | .unknown, | 5473 | .unknown, |
| 4729 | => false, | 5474 | => false, |
| ... | @@ -4747,6 +5492,7 @@ pub const FileExt = enum { | ... | @@ -4747,6 +5492,7 @@ pub const FileExt = enum { |
| 4747 | .static_library => target.staticLibSuffix(), | 5492 | .static_library => target.staticLibSuffix(), |
| 4748 | .zig => ".zig", | 5493 | .zig => ".zig", |
| 4749 | .def => ".def", | 5494 | .def => ".def", |
| 5495 | .rc => ".rc", | ||
| 4750 | .res => ".res", | 5496 | .res => ".res", |
| 4751 | .unknown => "", | 5497 | .unknown => "", |
| 4752 | }; | 5498 | }; |
| ... | @@ -4839,7 +5585,9 @@ pub fn classifyFileExt(filename: []const u8) FileExt { | ... | @@ -4839,7 +5585,9 @@ pub fn classifyFileExt(filename: []const u8) FileExt { |
| 4839 | return .cu; | 5585 | return .cu; |
| 4840 | } else if (mem.endsWith(u8, filename, ".def")) { | 5586 | } else if (mem.endsWith(u8, filename, ".def")) { |
| 4841 | return .def; | 5587 | 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")) { | ||
| 4843 | return .res; | 5591 | return .res; |
| 4844 | } else { | 5592 | } else { |
| 4845 | return .unknown; | 5593 | return .unknown; |
| ... | @@ -4983,6 +5731,13 @@ fn detectLibCFromLibCInstallation(arena: Allocator, target: Target, lci: *const | ... | @@ -4983,6 +5731,13 @@ fn detectLibCFromLibCInstallation(arena: Allocator, target: Target, lci: *const |
| 4983 | if (!is_redundant) list.appendAssumeCapacity(lci.sys_include_dir.?); | 5731 | if (!is_redundant) list.appendAssumeCapacity(lci.sys_include_dir.?); |
| 4984 | 5732 | ||
| 4985 | if (target.os.tag == .windows) { | 5733 | 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 | } | ||
| 4986 | if (std.fs.path.dirname(lci.include_dir.?)) |include_dir_parent| { | 5741 | if (std.fs.path.dirname(lci.include_dir.?)) |include_dir_parent| { |
| 4987 | const um_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_parent, "um" }); | 5742 | const um_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_parent, "um" }); |
| 4988 | list.appendAssumeCapacity(um_dir); | 5743 | list.appendAssumeCapacity(um_dir); |
src/link.zig+7-1| ... | @@ -1027,6 +1027,9 @@ pub const File = struct { | ... | @@ -1027,6 +1027,9 @@ pub const File = struct { |
| 1027 | for (comp.c_object_table.keys()) |key| { | 1027 | for (comp.c_object_table.keys()) |key| { |
| 1028 | _ = try man.addFile(key.status.success.object_path, null); | 1028 | _ = try man.addFile(key.status.success.object_path, null); |
| 1029 | } | 1029 | } |
| 1030 | for (comp.win32_resource_table.keys()) |key| { | ||
| 1031 | _ = try man.addFile(key.status.success.res_path, null); | ||
| 1032 | } | ||
| 1030 | try man.addOptionalFile(module_obj_path); | 1033 | try man.addOptionalFile(module_obj_path); |
| 1031 | try man.addOptionalFile(compiler_rt_path); | 1034 | try man.addOptionalFile(compiler_rt_path); |
| 1032 | 1035 | ||
| ... | @@ -1056,7 +1059,7 @@ pub const File = struct { | ... | @@ -1056,7 +1059,7 @@ pub const File = struct { |
| 1056 | }; | 1059 | }; |
| 1057 | } | 1060 | } |
| 1058 | 1061 | ||
| 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; |
| 1060 | var object_files = try std.ArrayList([*:0]const u8).initCapacity(base.allocator, num_object_files); | 1063 | var object_files = try std.ArrayList([*:0]const u8).initCapacity(base.allocator, num_object_files); |
| 1061 | defer object_files.deinit(); | 1064 | defer object_files.deinit(); |
| 1062 | 1065 | ||
| ... | @@ -1066,6 +1069,9 @@ pub const File = struct { | ... | @@ -1066,6 +1069,9 @@ pub const File = struct { |
| 1066 | for (comp.c_object_table.keys()) |key| { | 1069 | for (comp.c_object_table.keys()) |key| { |
| 1067 | object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.object_path)); | 1070 | object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.object_path)); |
| 1068 | } | 1071 | } |
| 1072 | for (comp.win32_resource_table.keys()) |key| { | ||
| 1073 | object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path)); | ||
| 1074 | } | ||
| 1069 | if (module_obj_path) |p| { | 1075 | if (module_obj_path) |p| { |
| 1070 | object_files.appendAssumeCapacity(try arena.dupeZ(u8, p)); | 1076 | object_files.appendAssumeCapacity(try arena.dupeZ(u8, p)); |
| 1071 | } | 1077 | } |
src/link/Coff/lld.zig+7| ... | @@ -72,6 +72,9 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod | ... | @@ -72,6 +72,9 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod |
| 72 | for (comp.c_object_table.keys()) |key| { | 72 | for (comp.c_object_table.keys()) |key| { |
| 73 | _ = try man.addFile(key.status.success.object_path, null); | 73 | _ = try man.addFile(key.status.success.object_path, null); |
| 74 | } | 74 | } |
| 75 | for (comp.win32_resource_table.keys()) |key| { | ||
| 76 | _ = try man.addFile(key.status.success.res_path, null); | ||
| 77 | } | ||
| 75 | try man.addOptionalFile(module_obj_path); | 78 | try man.addOptionalFile(module_obj_path); |
| 76 | man.hash.addOptionalBytes(self.base.options.entry); | 79 | man.hash.addOptionalBytes(self.base.options.entry); |
| 77 | man.hash.addOptional(self.base.options.stack_size_override); | 80 | 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 | ... | @@ -268,6 +271,10 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod |
| 268 | try argv.append(key.status.success.object_path); | 271 | try argv.append(key.status.success.object_path); |
| 269 | } | 272 | } |
| 270 | 273 | ||
| 274 | for (comp.win32_resource_table.keys()) |key| { | ||
| 275 | try argv.append(key.status.success.res_path); | ||
| 276 | } | ||
| 277 | |||
| 271 | if (module_obj_path) |p| { | 278 | if (module_obj_path) |p| { |
| 272 | try argv.append(p); | 279 | try argv.append(p); |
| 273 | } | 280 | } |
src/main.zig+68-2| ... | @@ -472,6 +472,12 @@ const usage_build_generic = | ... | @@ -472,6 +472,12 @@ const usage_build_generic = |
| 472 | \\ -D[macro]=[value] Define C [macro] to [value] (1 if [value] omitted) | 472 | \\ -D[macro]=[value] Define C [macro] to [value] (1 if [value] omitted) |
| 473 | \\ --libc [file] Provide a file which specifies libc paths | 473 | \\ --libc [file] Provide a file which specifies libc paths |
| 474 | \\ -cflags [flags] -- Set extra flags for the next positional C source files | 474 | \\ -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 | ||
| 475 | \\ | 481 | \\ |
| 476 | \\Link Options: | 482 | \\Link Options: |
| 477 | \\ -l[lib], --library [lib] Link against system library (only if actually used) | 483 | \\ -l[lib], --library [lib] Link against system library (only if actually used) |
| ... | @@ -919,11 +925,15 @@ fn buildOutputType( | ... | @@ -919,11 +925,15 @@ fn buildOutputType( |
| 919 | var wasi_emulated_libs = std.ArrayList(wasi_libc.CRTFile).init(arena); | 925 | var wasi_emulated_libs = std.ArrayList(wasi_libc.CRTFile).init(arena); |
| 920 | var clang_argv = std.ArrayList([]const u8).init(arena); | 926 | var clang_argv = std.ArrayList([]const u8).init(arena); |
| 921 | var extra_cflags = std.ArrayList([]const u8).init(arena); | 927 | var extra_cflags = std.ArrayList([]const u8).init(arena); |
| 928 | var extra_rcflags = std.ArrayList([]const u8).init(arena); | ||
| 922 | // These are before resolving sysroot. | 929 | // These are before resolving sysroot. |
| 923 | var lib_dir_args = std.ArrayList([]const u8).init(arena); | 930 | var lib_dir_args = std.ArrayList([]const u8).init(arena); |
| 924 | var rpath_list = std.ArrayList([]const u8).init(arena); | 931 | var rpath_list = std.ArrayList([]const u8).init(arena); |
| 925 | var symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .{}; | 932 | var symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .{}; |
| 926 | var c_source_files = std.ArrayList(Compilation.CSourceFile).init(arena); | 933 | 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); | ||
| 927 | var link_objects = std.ArrayList(Compilation.LinkObject).init(arena); | 937 | var link_objects = std.ArrayList(Compilation.LinkObject).init(arena); |
| 928 | var framework_dirs = std.ArrayList([]const u8).init(arena); | 938 | var framework_dirs = std.ArrayList([]const u8).init(arena); |
| 929 | var frameworks: std.StringArrayHashMapUnmanaged(Framework) = .{}; | 939 | var frameworks: std.StringArrayHashMapUnmanaged(Framework) = .{}; |
| ... | @@ -1042,6 +1052,19 @@ fn buildOutputType( | ... | @@ -1042,6 +1052,19 @@ fn buildOutputType( |
| 1042 | if (mem.eql(u8, next_arg, "--")) break; | 1052 | if (mem.eql(u8, next_arg, "--")) break; |
| 1043 | try extra_cflags.append(next_arg); | 1053 | try extra_cflags.append(next_arg); |
| 1044 | } | 1054 | } |
| 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 | } | ||
| 1045 | } else if (mem.eql(u8, arg, "--color")) { | 1068 | } else if (mem.eql(u8, arg, "--color")) { |
| 1046 | const next_arg = args_iter.next() orelse { | 1069 | const next_arg = args_iter.next() orelse { |
| 1047 | fatal("expected [auto|on|off] after --color", .{}); | 1070 | fatal("expected [auto|on|off] after --color", .{}); |
| ... | @@ -1590,7 +1613,8 @@ fn buildOutputType( | ... | @@ -1590,7 +1613,8 @@ fn buildOutputType( |
| 1590 | } | 1613 | } |
| 1591 | } else switch (file_ext orelse | 1614 | } else switch (file_ext orelse |
| 1592 | Compilation.classifyFileExt(arg)) { | 1615 | 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 }), | ||
| 1594 | .assembly, .assembly_with_cpp, .c, .cpp, .h, .ll, .bc, .m, .mm, .cu => { | 1618 | .assembly, .assembly_with_cpp, .c, .cpp, .h, .ll, .bc, .m, .mm, .cu => { |
| 1595 | try c_source_files.append(.{ | 1619 | try c_source_files.append(.{ |
| 1596 | .src_path = arg, | 1620 | .src_path = arg, |
| ... | @@ -1599,6 +1623,12 @@ fn buildOutputType( | ... | @@ -1599,6 +1623,12 @@ fn buildOutputType( |
| 1599 | .ext = file_ext, | 1623 | .ext = file_ext, |
| 1600 | }); | 1624 | }); |
| 1601 | }, | 1625 | }, |
| 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 | }, | ||
| 1602 | .zig => { | 1632 | .zig => { |
| 1603 | if (root_src_file) |other| { | 1633 | if (root_src_file) |other| { |
| 1604 | fatal("found another zig file '{s}' after root source file '{s}'", .{ arg, other }); | 1634 | fatal("found another zig file '{s}' after root source file '{s}'", .{ arg, other }); |
| ... | @@ -1684,13 +1714,20 @@ fn buildOutputType( | ... | @@ -1684,13 +1714,20 @@ fn buildOutputType( |
| 1684 | .ext = file_ext, // duped while parsing the args. | 1714 | .ext = file_ext, // duped while parsing the args. |
| 1685 | }); | 1715 | }); |
| 1686 | }, | 1716 | }, |
| 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(.{ | ||
| 1688 | .path = it.only_arg, | 1722 | .path = it.only_arg, |
| 1689 | .must_link = must_link, | 1723 | .must_link = must_link, |
| 1690 | }), | 1724 | }), |
| 1691 | .def => { | 1725 | .def => { |
| 1692 | linker_module_definition_file = it.only_arg; | 1726 | linker_module_definition_file = it.only_arg; |
| 1693 | }, | 1727 | }, |
| 1728 | .rc => { | ||
| 1729 | try rc_source_files.append(.{ .src_path = it.only_arg }); | ||
| 1730 | }, | ||
| 1694 | .zig => { | 1731 | .zig => { |
| 1695 | if (root_src_file) |other| { | 1732 | if (root_src_file) |other| { |
| 1696 | fatal("found another zig file '{s}' after root source file '{s}'", .{ it.only_arg, other }); | 1733 | fatal("found another zig file '{s}' after root source file '{s}'", .{ it.only_arg, other }); |
| ... | @@ -2452,6 +2489,12 @@ fn buildOutputType( | ... | @@ -2452,6 +2489,12 @@ fn buildOutputType( |
| 2452 | } else if (emit_bin == .yes) { | 2489 | } else if (emit_bin == .yes) { |
| 2453 | const basename = fs.path.basename(emit_bin.yes); | 2490 | const basename = fs.path.basename(emit_bin.yes); |
| 2454 | break :blk basename[0 .. basename.len - fs.path.extension(basename).len]; | 2491 | 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]; | ||
| 2455 | } else if (show_builtin) { | 2498 | } else if (show_builtin) { |
| 2456 | break :blk "builtin"; | 2499 | break :blk "builtin"; |
| 2457 | } else if (arg_mode == .run) { | 2500 | } else if (arg_mode == .run) { |
| ... | @@ -2530,6 +2573,21 @@ fn buildOutputType( | ... | @@ -2530,6 +2573,21 @@ fn buildOutputType( |
| 2530 | link_libcpp = true; | 2573 | link_libcpp = true; |
| 2531 | } | 2574 | } |
| 2532 | 2575 | ||
| 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 | |||
| 2533 | if (target_info.target.cpu.arch.isWasm()) blk: { | 2591 | if (target_info.target.cpu.arch.isWasm()) blk: { |
| 2534 | if (single_threaded == null) { | 2592 | if (single_threaded == null) { |
| 2535 | single_threaded = true; | 2593 | single_threaded = true; |
| ... | @@ -2933,6 +2991,7 @@ fn buildOutputType( | ... | @@ -2933,6 +2991,7 @@ fn buildOutputType( |
| 2933 | if (output_mode == .Obj and (object_format == .coff or object_format == .macho)) { | 2991 | if (output_mode == .Obj and (object_format == .coff or object_format == .macho)) { |
| 2934 | const total_obj_count = c_source_files.items.len + | 2992 | const total_obj_count = c_source_files.items.len + |
| 2935 | @intFromBool(root_src_file != null) + | 2993 | @intFromBool(root_src_file != null) + |
| 2994 | rc_source_files.items.len + | ||
| 2936 | link_objects.items.len; | 2995 | link_objects.items.len; |
| 2937 | if (total_obj_count > 1) { | 2996 | if (total_obj_count > 1) { |
| 2938 | fatal("{s} does not support linking multiple objects into one", .{@tagName(object_format)}); | 2997 | fatal("{s} does not support linking multiple objects into one", .{@tagName(object_format)}); |
| ... | @@ -3319,6 +3378,8 @@ fn buildOutputType( | ... | @@ -3319,6 +3378,8 @@ fn buildOutputType( |
| 3319 | .rpath_list = rpath_list.items, | 3378 | .rpath_list = rpath_list.items, |
| 3320 | .symbol_wrap_set = symbol_wrap_set, | 3379 | .symbol_wrap_set = symbol_wrap_set, |
| 3321 | .c_source_files = c_source_files.items, | 3380 | .c_source_files = c_source_files.items, |
| 3381 | .rc_source_files = rc_source_files.items, | ||
| 3382 | .rc_includes = rc_includes, | ||
| 3322 | .link_objects = link_objects.items, | 3383 | .link_objects = link_objects.items, |
| 3323 | .framework_dirs = framework_dirs.items, | 3384 | .framework_dirs = framework_dirs.items, |
| 3324 | .frameworks = resolved_frameworks.items, | 3385 | .frameworks = resolved_frameworks.items, |
| ... | @@ -6478,3 +6539,8 @@ fn accessFrameworkPath( | ... | @@ -6478,3 +6539,8 @@ fn accessFrameworkPath( |
| 6478 | 6539 | ||
| 6479 | return false; | 6540 | return false; |
| 6480 | } | 6541 | } |
| 6542 | |||
| 6543 | fn 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 @@ | ||
| 1 | pub const ani = @import("resinator/ani.zig"); | ||
| 2 | pub const ast = @import("resinator/ast.zig"); | ||
| 3 | pub const bmp = @import("resinator/bmp.zig"); | ||
| 4 | pub const cli = @import("resinator/cli.zig"); | ||
| 5 | pub const code_pages = @import("resinator/code_pages.zig"); | ||
| 6 | pub const comments = @import("resinator/comments.zig"); | ||
| 7 | pub const compile = @import("resinator/compile.zig"); | ||
| 8 | pub const errors = @import("resinator/errors.zig"); | ||
| 9 | pub const ico = @import("resinator/ico.zig"); | ||
| 10 | pub const lang = @import("resinator/lang.zig"); | ||
| 11 | pub const lex = @import("resinator/lex.zig"); | ||
| 12 | pub const literals = @import("resinator/literals.zig"); | ||
| 13 | pub const parse = @import("resinator/parse.zig"); | ||
| 14 | pub const rc = @import("resinator/rc.zig"); | ||
| 15 | pub const res = @import("resinator/res.zig"); | ||
| 16 | pub const source_mapping = @import("resinator/source_mapping.zig"); | ||
| 17 | pub const utils = @import("resinator/utils.zig"); | ||
| 18 | pub 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 | |||
| 15 | const std = @import("std"); | ||
| 16 | |||
| 17 | const AF_ICON: u32 = 1; | ||
| 18 | |||
| 19 | pub fn isAnimatedIcon(reader: anytype) bool { | ||
| 20 | const flags = getAniheaderFlags(reader) catch return false; | ||
| 21 | return flags & AF_ICON == AF_ICON; | ||
| 22 | } | ||
| 23 | |||
| 24 | fn 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 | ||
| 48 | const 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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const Allocator = std.mem.Allocator; | ||
| 3 | const Token = @import("lex.zig").Token; | ||
| 4 | const CodePage = @import("code_pages.zig").CodePage; | ||
| 5 | |||
| 6 | pub 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 | |||
| 30 | pub 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 | |||
| 83 | test "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 | |||
| 106 | pub 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 | |||
| 21 | const std = @import("std"); | ||
| 22 | const BitmapHeader = @import("ico.zig").BitmapHeader; | ||
| 23 | |||
| 24 | pub const windows_format_id = std.mem.readIntNative(u16, "BM"); | ||
| 25 | pub const file_header_len = 14; | ||
| 26 | |||
| 27 | pub const ReadError = error{ | ||
| 28 | UnexpectedEOF, | ||
| 29 | InvalidFileHeader, | ||
| 30 | ImpossiblePixelDataOffset, | ||
| 31 | UnknownBitmapVersion, | ||
| 32 | InvalidBitsPerPixel, | ||
| 33 | TooManyColorsInPalette, | ||
| 34 | MissingBitfieldMasks, | ||
| 35 | }; | ||
| 36 | |||
| 37 | pub 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 | |||
| 88 | pub 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 | ||
| 143 | pub 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 | ||
| 152 | pub 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 | |||
| 210 | pub 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 | |||
| 224 | fn 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 | |||
| 230 | test "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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const CodePage = @import("code_pages.zig").CodePage; | ||
| 3 | const lang = @import("lang.zig"); | ||
| 4 | const res = @import("res.zig"); | ||
| 5 | const Allocator = std.mem.Allocator; | ||
| 6 | const lex = @import("lex.zig"); | ||
| 7 | |||
| 8 | /// This is what /SL 100 will set the maximum string literal length to | ||
| 9 | pub const max_string_literal_length_100_percent = 8192; | ||
| 10 | |||
| 11 | pub 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 | |||
| 60 | pub 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 | |||
| 119 | pub 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 | |||
| 290 | pub 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 | |||
| 380 | pub 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. | ||
| 384 | pub 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 | ||
| 897 | pub 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 | ||
| 912 | pub 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 | |||
| 954 | test 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 | |||
| 967 | pub 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 | |||
| 1073 | fn testParse(args: []const []const u8) !Options { | ||
| 1074 | return (try testParseOutput(args, "")).?; | ||
| 1075 | } | ||
| 1076 | |||
| 1077 | fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options { | ||
| 1078 | return (try testParseOutput(args, expected_output)).?; | ||
| 1079 | } | ||
| 1080 | |||
| 1081 | fn 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 | |||
| 1090 | fn 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 | |||
| 1112 | test "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 | |||
| 1198 | test "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 | |||
| 1213 | test "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 | |||
| 1251 | test "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 | |||
| 1310 | test "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 | |||
| 1353 | test "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 | |||
| 1374 | test "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 | |||
| 1413 | test "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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const 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 | ||
| 78 | pub 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 | |||
| 279 | pub 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 | |||
| 368 | test "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 | |||
| 375 | test "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 | |||
| 442 | test "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 | |||
| 464 | test "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 | |||
| 482 | pub 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 | |||
| 16 | const std = @import("std"); | ||
| 17 | const Allocator = std.mem.Allocator; | ||
| 18 | const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter; | ||
| 19 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | ||
| 20 | const LineHandler = @import("lex.zig").LineHandler; | ||
| 21 | const 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) | ||
| 25 | pub 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 | |||
| 181 | inline 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 | |||
| 208 | pub 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 | |||
| 215 | fn 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 | |||
| 222 | test "basic" { | ||
| 223 | try testRemoveComments("", "// comment"); | ||
| 224 | try testRemoveComments("", "/* comment */"); | ||
| 225 | } | ||
| 226 | |||
| 227 | test "mixed" { | ||
| 228 | try testRemoveComments("hello", "hello// comment"); | ||
| 229 | try testRemoveComments("hello", "hel/* comment */lo"); | ||
| 230 | } | ||
| 231 | |||
| 232 | test "within a string" { | ||
| 233 | // escaped " is \" | ||
| 234 | try testRemoveComments( | ||
| 235 | \\blah"//som\"/*ething*/"BLAH | ||
| 236 | , | ||
| 237 | \\blah"//som\"/*ething*/"BLAH | ||
| 238 | ); | ||
| 239 | } | ||
| 240 | |||
| 241 | test "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 | |||
| 255 | test "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 | |||
| 277 | test "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 | |||
| 305 | test "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 | |||
| 319 | test "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 | |||
| 336 | test "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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | const Node = @import("ast.zig").Node; | ||
| 5 | const lex = @import("lex.zig"); | ||
| 6 | const Parser = @import("parse.zig").Parser; | ||
| 7 | const Resource = @import("rc.zig").Resource; | ||
| 8 | const Token = @import("lex.zig").Token; | ||
| 9 | const literals = @import("literals.zig"); | ||
| 10 | const Number = literals.Number; | ||
| 11 | const SourceBytes = literals.SourceBytes; | ||
| 12 | const Diagnostics = @import("errors.zig").Diagnostics; | ||
| 13 | const ErrorDetails = @import("errors.zig").ErrorDetails; | ||
| 14 | const MemoryFlags = @import("res.zig").MemoryFlags; | ||
| 15 | const rc = @import("rc.zig"); | ||
| 16 | const res = @import("res.zig"); | ||
| 17 | const ico = @import("ico.zig"); | ||
| 18 | const ani = @import("ani.zig"); | ||
| 19 | const bmp = @import("bmp.zig"); | ||
| 20 | const WORD = std.os.windows.WORD; | ||
| 21 | const DWORD = std.os.windows.DWORD; | ||
| 22 | const utils = @import("utils.zig"); | ||
| 23 | const NameOrOrdinal = res.NameOrOrdinal; | ||
| 24 | const CodePage = @import("code_pages.zig").CodePage; | ||
| 25 | const CodePageLookup = @import("ast.zig").CodePageLookup; | ||
| 26 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | ||
| 27 | const windows1252 = @import("windows1252.zig"); | ||
| 28 | const lang = @import("lang.zig"); | ||
| 29 | const code_pages = @import("code_pages.zig"); | ||
| 30 | const errors = @import("errors.zig"); | ||
| 31 | |||
| 32 | pub 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 | |||
| 60 | pub 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 | |||
| 150 | pub 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 | |||
| 2808 | pub const OpenSearchPathError = std.fs.Dir.OpenError; | ||
| 2809 | |||
| 2810 | fn 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. | ||
| 2823 | fn 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 | |||
| 2842 | pub 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` | ||
| 2855 | pub 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 | |||
| 2881 | pub 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. | ||
| 2890 | pub 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 | ||
| 2915 | pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) { | ||
| 2916 | return .{ .inner_writer = inner_writer, .bytes_left = bytes_left }; | ||
| 2917 | } | ||
| 2918 | |||
| 2919 | test "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 | |||
| 2933 | pub 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 | |||
| 3037 | pub 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 | |||
| 3068 | pub 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 | |||
| 3301 | test "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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const Token = @import("lex.zig").Token; | ||
| 3 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | ||
| 4 | const utils = @import("utils.zig"); | ||
| 5 | const rc = @import("rc.zig"); | ||
| 6 | const res = @import("res.zig"); | ||
| 7 | const ico = @import("ico.zig"); | ||
| 8 | const bmp = @import("bmp.zig"); | ||
| 9 | const parse = @import("parse.zig"); | ||
| 10 | const CodePage = @import("code_pages.zig").CodePage; | ||
| 11 | |||
| 12 | pub 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 | ||
| 91 | pub const DiagnosticsContext = struct { | ||
| 92 | diagnostics: *Diagnostics, | ||
| 93 | token: Token, | ||
| 94 | }; | ||
| 95 | |||
| 96 | pub 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 | |||
| 803 | pub 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 | |||
| 935 | const 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 | |||
| 989 | fn writeSourceSlice(writer: anytype, slice: []const u8) !void { | ||
| 990 | for (slice) |c| try writeSourceByte(writer, c); | ||
| 991 | } | ||
| 992 | |||
| 993 | inline 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 | |||
| 1009 | pub 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 | |||
| 1028 | pub 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 | |||
| 7 | const std = @import("std"); | ||
| 8 | |||
| 9 | pub const ReadError = std.mem.Allocator.Error || error{ InvalidHeader, InvalidImageType, ImpossibleDataSize, UnexpectedEOF, ReadError }; | ||
| 10 | |||
| 11 | pub 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. | ||
| 39 | pub 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 | |||
| 111 | pub const ImageType = enum(u16) { | ||
| 112 | icon = 1, | ||
| 113 | cursor = 2, | ||
| 114 | }; | ||
| 115 | |||
| 116 | pub 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 | |||
| 149 | pub 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 | |||
| 196 | test "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 | |||
| 206 | test "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 | |||
| 215 | test "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 | |||
| 221 | test "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 | |||
| 227 | test "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 | |||
| 233 | pub 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). | ||
| 261 | pub 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 @@ | ||
| 1 | const 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 | ||
| 10 | pub 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 | |||
| 56 | test 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. | ||
| 74 | pub 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 | |||
| 83 | pub 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 | |||
| 113 | test 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 | |||
| 121 | test "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 | |||
| 160 | fn 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." | ||
| 189 | pub const LOCALE_CUSTOM_UNSPECIFIED = 0x1000; | ||
| 190 | |||
| 191 | pub const LANG_ENGLISH = 0x09; | ||
| 192 | pub const SUBLANG_ENGLISH_US = 0x01; | ||
| 193 | |||
| 194 | /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers | ||
| 195 | pub 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` | ||
| 217 | pub 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 | |||
| 300 | pub 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." | ||
| 332 | const 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 | |||
| 353 | test "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 | |||
| 423 | fn isAllAlphabetic(str: []const u8) bool { | ||
| 424 | for (str) |c| { | ||
| 425 | if (!std.ascii.isAlphabetic(c)) return false; | ||
| 426 | } | ||
| 427 | return true; | ||
| 428 | } | ||
| 429 | |||
| 430 | fn isAllAlphanumeric(str: []const u8) bool { | ||
| 431 | for (str) |c| { | ||
| 432 | if (!std.ascii.isAlphanumeric(c)) return false; | ||
| 433 | } | ||
| 434 | return true; | ||
| 435 | } | ||
| 436 | |||
| 437 | fn 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). | ||
| 454 | pub 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 | |||
| 7 | const std = @import("std"); | ||
| 8 | const ErrorDetails = @import("errors.zig").ErrorDetails; | ||
| 9 | const columnsUntilTabStop = @import("literals.zig").columnsUntilTabStop; | ||
| 10 | const code_pages = @import("code_pages.zig"); | ||
| 11 | const CodePage = code_pages.CodePage; | ||
| 12 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | ||
| 13 | const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit; | ||
| 14 | |||
| 15 | const dumpTokensDuringTests = false; | ||
| 16 | |||
| 17 | pub const default_max_string_literal_codepoints = 4097; | ||
| 18 | |||
| 19 | pub 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 | |||
| 123 | pub 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 | |||
| 181 | pub 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 | |||
| 203 | pub 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 | |||
| 1043 | fn 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 | |||
| 1055 | fn expectLexError(expected: LexError, actual: anytype) !void { | ||
| 1056 | try std.testing.expectError(expected, actual); | ||
| 1057 | if (dumpTokensDuringTests) std.debug.print("{!}\n", .{actual}); | ||
| 1058 | } | ||
| 1059 | |||
| 1060 | test "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 | |||
| 1067 | test "normal: string literals" { | ||
| 1068 | try testLexNormal("\"\"", &.{.quoted_ascii_string}); | ||
| 1069 | // "" is an escaped " | ||
| 1070 | try testLexNormal("\" \"\" \"", &.{.quoted_ascii_string}); | ||
| 1071 | } | ||
| 1072 | |||
| 1073 | test "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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const code_pages = @import("code_pages.zig"); | ||
| 3 | const CodePage = code_pages.CodePage; | ||
| 4 | const windows1252 = @import("windows1252.zig"); | ||
| 5 | const ErrorDetails = @import("errors.zig").ErrorDetails; | ||
| 6 | const DiagnosticsContext = @import("errors.zig").DiagnosticsContext; | ||
| 7 | const 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. | ||
| 11 | pub 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 | |||
| 19 | pub const SourceBytes = struct { | ||
| 20 | slice: []const u8, | ||
| 21 | code_page: CodePage, | ||
| 22 | }; | ||
| 23 | |||
| 24 | pub 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. | ||
| 54 | pub 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 | |||
| 372 | pub const StringParseOptions = struct { | ||
| 373 | start_column: usize = 0, | ||
| 374 | diagnostics: ?DiagnosticsContext = null, | ||
| 375 | output_code_page: CodePage = .windows1252, | ||
| 376 | }; | ||
| 377 | |||
| 378 | pub 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 | |||
| 446 | pub 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 | |||
| 451 | pub 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 | |||
| 456 | pub 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 | |||
| 461 | pub 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 | |||
| 466 | test "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 | |||
| 606 | test "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 | |||
| 656 | test "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 | |||
| 715 | test "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 | |||
| 738 | test "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 | |||
| 772 | pub 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 | |||
| 778 | pub 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) | ||
| 809 | pub 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 | |||
| 871 | test "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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const Lexer = @import("lex.zig").Lexer; | ||
| 3 | const Token = @import("lex.zig").Token; | ||
| 4 | const Node = @import("ast.zig").Node; | ||
| 5 | const Tree = @import("ast.zig").Tree; | ||
| 6 | const CodePageLookup = @import("ast.zig").CodePageLookup; | ||
| 7 | const Resource = @import("rc.zig").Resource; | ||
| 8 | const Allocator = std.mem.Allocator; | ||
| 9 | const ErrorDetails = @import("errors.zig").ErrorDetails; | ||
| 10 | const Diagnostics = @import("errors.zig").Diagnostics; | ||
| 11 | const SourceBytes = @import("literals.zig").SourceBytes; | ||
| 12 | const Compiler = @import("compile.zig").Compiler; | ||
| 13 | const rc = @import("rc.zig"); | ||
| 14 | const res = @import("res.zig"); | ||
| 15 | |||
| 16 | // TODO: Make these configurable? | ||
| 17 | pub const max_nested_menu_level: u32 = 512; | ||
| 18 | pub const max_nested_version_level: u32 = 512; | ||
| 19 | pub const max_nested_expression_level: u32 = 200; | ||
| 20 | |||
| 21 | pub 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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const utils = @import("utils.zig"); | ||
| 3 | const res = @import("res.zig"); | ||
| 4 | const SourceBytes = @import("literals.zig").SourceBytes; | ||
| 5 | |||
| 6 | // https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files | ||
| 7 | |||
| 8 | pub 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 | ||
| 147 | pub 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 | |||
| 176 | pub 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 | |||
| 233 | pub 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 | |||
| 264 | const 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 | |||
| 279 | pub 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 | |||
| 311 | pub 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 | |||
| 321 | pub 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 | |||
| 341 | pub 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. | ||
| 353 | pub 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 | |||
| 367 | pub 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 | |||
| 391 | pub 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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const rc = @import("rc.zig"); | ||
| 3 | const Resource = rc.Resource; | ||
| 4 | const CommonResourceAttributes = rc.CommonResourceAttributes; | ||
| 5 | const Allocator = std.mem.Allocator; | ||
| 6 | const windows1252 = @import("windows1252.zig"); | ||
| 7 | const CodePage = @import("code_pages.zig").CodePage; | ||
| 8 | const literals = @import("literals.zig"); | ||
| 9 | const SourceBytes = literals.SourceBytes; | ||
| 10 | const Codepoint = @import("code_pages.zig").Codepoint; | ||
| 11 | const lang = @import("lang.zig"); | ||
| 12 | const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit; | ||
| 13 | |||
| 14 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types | ||
| 15 | pub 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 | ||
| 79 | pub 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 | ||
| 148 | pub 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 | ||
| 168 | pub 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 | |||
| 220 | pub 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 | |||
| 429 | fn 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 | |||
| 442 | test "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 | |||
| 537 | test "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 | ||
| 565 | pub 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 | |||
| 605 | const 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 | |||
| 618 | pub 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""). | ||
| 621 | pub 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 | |||
| 699 | test "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 | |||
| 879 | pub 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 | |||
| 917 | test "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 | ||
| 937 | pub 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 | |||
| 983 | test "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 | |||
| 991 | pub const VersionNode = struct { | ||
| 992 | pub const type_string: u16 = 1; | ||
| 993 | pub const type_binary: u16 = 0; | ||
| 994 | }; | ||
| 995 | |||
| 996 | pub 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 | ||
| 1025 | pub 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 | ||
| 1037 | pub 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 | ||
| 1072 | pub 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 | ||
| 1078 | pub 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 | ||
| 1097 | pub 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 | ||
| 1106 | pub const LBS = struct { | ||
| 1107 | pub const NOTIFY: u32 = 0x0001; | ||
| 1108 | }; | ||
src/resinator/source_mapping.zig created+684| ... | @@ -0,0 +1,684 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const Allocator = std.mem.Allocator; | ||
| 3 | const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter; | ||
| 4 | const parseQuotedAsciiString = @import("literals.zig").parseQuotedAsciiString; | ||
| 5 | const lex = @import("lex.zig"); | ||
| 6 | |||
| 7 | pub const ParseLineCommandsResult = struct { | ||
| 8 | result: []u8, | ||
| 9 | mappings: SourceMappings, | ||
| 10 | }; | ||
| 11 | |||
| 12 | const CurrentMapping = struct { | ||
| 13 | line_num: usize = 1, | ||
| 14 | filename: std.ArrayListUnmanaged(u8) = .{}, | ||
| 15 | pending: bool = true, | ||
| 16 | ignore_contents: bool = false, | ||
| 17 | }; | ||
| 18 | |||
| 19 | pub 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. | ||
| 42 | pub 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 | ||
| 196 | pub 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 | |||
| 208 | pub 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 | ||
| 222 | pub 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 | |||
| 250 | pub 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. | ||
| 268 | fn 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 | |||
| 399 | fn 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 | |||
| 406 | test 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 | |||
| 420 | pub 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 | |||
| 490 | test "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 | ||
| 510 | pub 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 | |||
| 554 | const ExpectedSourceSpan = struct { | ||
| 555 | start_line: usize, | ||
| 556 | end_line: usize, | ||
| 557 | filename: []const u8, | ||
| 558 | }; | ||
| 559 | |||
| 560 | fn 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 | |||
| 589 | fn 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 | |||
| 601 | test "basic" { | ||
| 602 | try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{ | ||
| 603 | .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" }, | ||
| 604 | }, "#line 1 \"blah.rc\"", .{}); | ||
| 605 | } | ||
| 606 | |||
| 607 | test "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 | |||
| 618 | test "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 | |||
| 624 | test "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 | |||
| 653 | test "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 | |||
| 666 | test "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 | |||
| 679 | test "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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | |||
| 4 | /// Like std.io.FixedBufferStream but does no bounds checking | ||
| 5 | pub 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. | ||
| 30 | pub 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. | ||
| 45 | pub 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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | pub 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 | ||
| 24 | pub 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 | ||
| 35 | pub 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 | ||
| 81 | pub 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 | |||
| 576 | test "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{ | ... | @@ -194,6 +194,10 @@ pub const build_cases = [_]BuildCase{ |
| 194 | .build_root = "test/standalone/load_dynamic_library", | 194 | .build_root = "test/standalone/load_dynamic_library", |
| 195 | .import = @import("standalone/load_dynamic_library/build.zig"), | 195 | .import = @import("standalone/load_dynamic_library/build.zig"), |
| 196 | }, | 196 | }, |
| 197 | .{ | ||
| 198 | .build_root = "test/standalone/windows_resources", | ||
| 199 | .import = @import("standalone/windows_resources/build.zig"), | ||
| 200 | }, | ||
| 197 | .{ | 201 | .{ |
| 198 | .build_root = "test/standalone/windows_spawn", | 202 | .build_root = "test/standalone/windows_spawn", |
| 199 | .import = @import("standalone/windows_spawn/build.zig"), | 203 | .import = @import("standalone/windows_spawn/build.zig"), |
test/standalone/windows_resources/build.zig created+40| ... | @@ -0,0 +1,40 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | pub 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 | |||
| 21 | fn 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 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | pub 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 @@ | ||
| 1 | abcdefg | ||
| \ No newline at end of file | |||
test/standalone/windows_resources/res/sub/sub.rc created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | 2 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 | |||
| 9 | ICO_ID ICON "zig.ico" | ||
| 10 | |||
| 11 | 1 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 | ||
| 19 | BEGIN | ||
| 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 | ||
| 38 | END | ||
| 39 | |||
| 40 | #include "sub/sub.rc" | ||