res.zig (49608B) - Raw
1 const std = @import("std"); 2 const assert = std.debug.assert; 3 const rc = @import("rc.zig"); 4 const ResourceType = rc.ResourceType; 5 const CommonResourceAttributes = rc.CommonResourceAttributes; 6 const Allocator = std.mem.Allocator; 7 const windows1252 = @import("windows1252.zig"); 8 const SupportedCodePage = @import("code_pages.zig").SupportedCodePage; 9 const literals = @import("literals.zig"); 10 const SourceBytes = literals.SourceBytes; 11 const Codepoint = @import("code_pages.zig").Codepoint; 12 const lang = @import("lang.zig"); 13 const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit; 14 15 /// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types 16 pub const RT = enum(u8) { 17 ACCELERATOR = 9, 18 ANICURSOR = 21, 19 ANIICON = 22, 20 BITMAP = 2, 21 CURSOR = 1, 22 DIALOG = 5, 23 DLGINCLUDE = 17, 24 DLGINIT = 240, 25 FONT = 8, 26 FONTDIR = 7, 27 GROUP_CURSOR = 1 + 11, // CURSOR + 11 28 GROUP_ICON = 3 + 11, // ICON + 11 29 HTML = 23, 30 ICON = 3, 31 MANIFEST = 24, 32 MENU = 4, 33 MESSAGETABLE = 11, 34 PLUGPLAY = 19, 35 RCDATA = 10, 36 STRING = 6, 37 TOOLBAR = 241, 38 VERSION = 16, 39 VXD = 20, 40 _, 41 42 /// Returns null if the resource type is user-defined 43 /// Asserts that the resource is not `stringtable` 44 pub fn fromResource(resource: ResourceType) ?RT { 45 return switch (resource) { 46 .accelerators => .ACCELERATOR, 47 .bitmap => .BITMAP, 48 .cursor => .GROUP_CURSOR, 49 .dialog => .DIALOG, 50 .dialogex => .DIALOG, 51 .dlginclude => .DLGINCLUDE, 52 .dlginit => .DLGINIT, 53 .font => .FONT, 54 .html => .HTML, 55 .icon => .GROUP_ICON, 56 .menu => .MENU, 57 .menuex => .MENU, 58 .messagetable => .MESSAGETABLE, 59 .plugplay => .PLUGPLAY, 60 .rcdata => .RCDATA, 61 .stringtable => unreachable, 62 .toolbar => .TOOLBAR, 63 .user_defined => null, 64 .versioninfo => .VERSION, 65 .vxd => .VXD, 66 67 .cursor_num => .CURSOR, 68 .icon_num => .ICON, 69 .string_num => .STRING, 70 .anicursor_num => .ANICURSOR, 71 .aniicon_num => .ANIICON, 72 .fontdir_num => .FONTDIR, 73 .manifest_num => .MANIFEST, 74 }; 75 } 76 }; 77 78 /// https://learn.microsoft.com/en-us/windows/win32/menurc/common-resource-attributes 79 /// https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader 80 pub const MemoryFlags = packed struct(u16) { 81 value: u16, 82 83 pub const MOVEABLE: u16 = 0x10; 84 // TODO: SHARED and PURE seem to be the same thing? Testing seems to confirm this but 85 // would like to find mention of it somewhere. 86 pub const SHARED: u16 = 0x20; 87 pub const PURE: u16 = 0x20; 88 pub const PRELOAD: u16 = 0x40; 89 pub const DISCARDABLE: u16 = 0x1000; 90 91 /// Note: The defaults can have combinations that are not possible to specify within 92 /// an .rc file, as the .rc attributes imply other values (i.e. specifying 93 /// DISCARDABLE always implies MOVEABLE and PURE/SHARED, and yet RT_ICON 94 /// has a default of only MOVEABLE | DISCARDABLE). 95 pub fn defaults(predefined_resource_type: ?RT) MemoryFlags { 96 if (predefined_resource_type == null) { 97 return MemoryFlags{ .value = MOVEABLE | SHARED }; 98 } else { 99 return switch (predefined_resource_type.?) { 100 // zig fmt: off 101 .RCDATA, .BITMAP, .HTML, .MANIFEST, 102 .ACCELERATOR, .VERSION, .MESSAGETABLE, 103 .DLGINIT, .TOOLBAR, .PLUGPLAY, 104 .VXD, => MemoryFlags{ .value = MOVEABLE | SHARED }, 105 106 .GROUP_ICON, .GROUP_CURSOR, 107 .STRING, .FONT, .DIALOG, .MENU, 108 .DLGINCLUDE, => MemoryFlags{ .value = MOVEABLE | SHARED | DISCARDABLE }, 109 110 .ICON, .CURSOR, .ANIICON, .ANICURSOR => MemoryFlags{ .value = MOVEABLE | DISCARDABLE }, 111 .FONTDIR => MemoryFlags{ .value = MOVEABLE | PRELOAD }, 112 // zig fmt: on 113 // Same as predefined_resource_type == null 114 _ => return MemoryFlags{ .value = MOVEABLE | SHARED }, 115 }; 116 } 117 } 118 119 pub fn set(self: *MemoryFlags, attribute: CommonResourceAttributes) void { 120 switch (attribute) { 121 .preload => self.value |= PRELOAD, 122 .loadoncall => self.value &= ~PRELOAD, 123 .moveable => self.value |= MOVEABLE, 124 .fixed => self.value &= ~(MOVEABLE | DISCARDABLE), 125 .shared => self.value |= SHARED, 126 .nonshared => self.value &= ~(SHARED | DISCARDABLE), 127 .pure => self.value |= PURE, 128 .impure => self.value &= ~(PURE | DISCARDABLE), 129 .discardable => self.value |= DISCARDABLE | MOVEABLE | PURE, 130 } 131 } 132 133 pub fn setGroup(self: *MemoryFlags, attribute: CommonResourceAttributes, implied_shared_or_pure: bool) void { 134 switch (attribute) { 135 .preload => { 136 self.value |= PRELOAD; 137 if (implied_shared_or_pure) self.value &= ~SHARED; 138 }, 139 .loadoncall => { 140 self.value &= ~PRELOAD; 141 if (implied_shared_or_pure) self.value |= SHARED; 142 }, 143 else => self.set(attribute), 144 } 145 } 146 }; 147 148 /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers 149 pub const Language = packed struct(u16) { 150 // Note: This is the default no matter what locale the current system is set to, 151 // e.g. even if the system's locale is en-GB, en-US will still be the 152 // default language for resources in the Win32 rc compiler. 153 primary_language_id: u10 = lang.LANG_ENGLISH, 154 sublanguage_id: u6 = lang.SUBLANG_ENGLISH_US, 155 156 /// Default language ID as a u16 157 pub const default: u16 = (Language{}).asInt(); 158 159 pub fn fromInt(int: u16) Language { 160 return @bitCast(int); 161 } 162 163 pub fn asInt(self: Language) u16 { 164 return @bitCast(self); 165 } 166 167 pub fn format(language: Language, w: *std.io.Writer) std.io.Writer.Error!void { 168 const language_id = language.asInt(); 169 const language_name = language_name: { 170 if (std.enums.fromInt(lang.LanguageId, language_id)) |lang_enum_val| { 171 break :language_name @tagName(lang_enum_val); 172 } 173 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) { 174 break :language_name "LOCALE_CUSTOM_UNSPECIFIED"; 175 } 176 break :language_name "<UNKNOWN>"; 177 }; 178 try w.print("{s} (0x{X})", .{ language_name, language_id }); 179 } 180 }; 181 182 /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks 183 pub const ControlClass = enum(u16) { 184 button = 0x80, 185 edit = 0x81, 186 static = 0x82, 187 listbox = 0x83, 188 scrollbar = 0x84, 189 combobox = 0x85, 190 191 pub fn fromControl(control: rc.Control) ?ControlClass { 192 return switch (control) { 193 // zig fmt: off 194 .auto3state, .autocheckbox, .autoradiobutton, 195 .checkbox, .defpushbutton, .groupbox, .pushbox, 196 .pushbutton, .radiobutton, .state3, .userbutton => .button, 197 // zig fmt: on 198 .combobox => .combobox, 199 .control => null, 200 .ctext, .icon, .ltext, .rtext => .static, 201 .edittext, .hedit, .iedit => .edit, 202 .listbox => .listbox, 203 .scrollbar => .scrollbar, 204 }; 205 } 206 207 pub fn getImpliedStyle(control: rc.Control) u32 { 208 var style = WS.CHILD | WS.VISIBLE; 209 switch (control) { 210 .auto3state => style |= BS.AUTO3STATE | WS.TABSTOP, 211 .autocheckbox => style |= BS.AUTOCHECKBOX | WS.TABSTOP, 212 .autoradiobutton => style |= BS.AUTORADIOBUTTON, 213 .checkbox => style |= BS.CHECKBOX | WS.TABSTOP, 214 .combobox => {}, 215 .control => {}, 216 .ctext => style |= SS.CENTER | WS.GROUP, 217 .defpushbutton => style |= BS.DEFPUSHBUTTON | WS.TABSTOP, 218 .edittext, .hedit, .iedit => style |= WS.TABSTOP | WS.BORDER, 219 .groupbox => style |= BS.GROUPBOX, 220 .icon => style |= SS.ICON, 221 .listbox => style |= LBS.NOTIFY | WS.BORDER, 222 .ltext => style |= WS.GROUP, 223 .pushbox => style |= BS.PUSHBOX | WS.TABSTOP, 224 .pushbutton => style |= WS.TABSTOP, 225 .radiobutton => style |= BS.RADIOBUTTON, 226 .rtext => style |= SS.RIGHT | WS.GROUP, 227 .scrollbar => {}, 228 .state3 => style |= BS.@"3STATE" | WS.TABSTOP, 229 .userbutton => style |= BS.USERBUTTON | WS.TABSTOP, 230 } 231 return style; 232 } 233 }; 234 235 pub const NameOrOrdinal = union(enum) { 236 // UTF-16 LE 237 name: [:0]const u16, 238 ordinal: u16, 239 240 pub fn deinit(self: NameOrOrdinal, allocator: Allocator) void { 241 switch (self) { 242 .name => |name| { 243 allocator.free(name); 244 }, 245 .ordinal => {}, 246 } 247 } 248 249 /// Returns the full length of the amount of bytes that would be written by `write` 250 /// (e.g. for an ordinal it will return the length including the 0xFFFF indicator) 251 pub fn byteLen(self: NameOrOrdinal) usize { 252 switch (self) { 253 .name => |name| { 254 // + 1 for 0-terminated 255 return (name.len + 1) * @sizeOf(u16); 256 }, 257 .ordinal => return 4, 258 } 259 } 260 261 pub fn write(self: NameOrOrdinal, writer: anytype) !void { 262 switch (self) { 263 .name => |name| { 264 try writer.writeAll(std.mem.sliceAsBytes(name[0 .. name.len + 1])); 265 }, 266 .ordinal => |ordinal| { 267 try writer.writeInt(u16, 0xffff, .little); 268 try writer.writeInt(u16, ordinal, .little); 269 }, 270 } 271 } 272 273 pub fn writeEmpty(writer: anytype) !void { 274 try writer.writeInt(u16, 0, .little); 275 } 276 277 pub fn fromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal { 278 if (maybeOrdinalFromString(bytes)) |ordinal| { 279 return ordinal; 280 } 281 return nameFromString(allocator, bytes); 282 } 283 284 pub fn nameFromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal { 285 // Names have a limit of 256 UTF-16 code units + null terminator 286 var buf = try std.array_list.Managed(u16).initCapacity(allocator, @min(257, bytes.slice.len)); 287 errdefer buf.deinit(); 288 289 var i: usize = 0; 290 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) { 291 if (buf.items.len == 256) break; 292 293 const c = codepoint.value; 294 if (c == Codepoint.invalid) { 295 try buf.append(std.mem.nativeToLittle(u16, '�')); 296 } else if (c < 0x7F) { 297 // ASCII chars in names are always converted to uppercase 298 try buf.append(std.mem.nativeToLittle(u16, std.ascii.toUpper(@intCast(c)))); 299 } else if (c < 0x10000) { 300 const short: u16 = @intCast(c); 301 try buf.append(std.mem.nativeToLittle(u16, short)); 302 } else { 303 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800; 304 try buf.append(std.mem.nativeToLittle(u16, high)); 305 306 // Note: This can cut-off in the middle of a UTF-16 surrogate pair, 307 // i.e. it can make the string end with an unpaired high surrogate 308 if (buf.items.len == 256) break; 309 310 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00; 311 try buf.append(std.mem.nativeToLittle(u16, low)); 312 } 313 } 314 315 return NameOrOrdinal{ .name = try buf.toOwnedSliceSentinel(0) }; 316 } 317 318 /// Returns `null` if the bytes do not form a valid number. 319 /// Does not allow non-ASCII digits (which the Win32 RC compiler does allow 320 /// in base 10 numbers, see `maybeNonAsciiOrdinalFromString`). 321 pub fn maybeOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal { 322 var buf = bytes.slice; 323 var radix: u8 = 10; 324 if (buf.len > 2 and buf[0] == '0') { 325 switch (buf[1]) { 326 '0'...'9' => {}, 327 'x', 'X' => { 328 radix = 16; 329 buf = buf[2..]; 330 // only the first 4 hex digits matter, anything else is ignored 331 // i.e. 0x12345 is treated as if it were 0x1234 332 buf.len = @min(buf.len, 4); 333 }, 334 else => return null, 335 } 336 } 337 338 var i: usize = 0; 339 var result: u16 = 0; 340 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) { 341 const c = codepoint.value; 342 const digit: u8 = switch (c) { 343 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch switch (radix) { 344 10 => return null, 345 // non-hex-digits are treated as a terminator rather than invalidating 346 // the number (note: if there are no valid hex digits then the result 347 // will be zero which is not treated as a valid number) 348 16 => break, 349 else => unreachable, 350 }, 351 else => if (radix == 10) return null else break, 352 }; 353 354 if (result != 0) { 355 result *%= radix; 356 } 357 result +%= digit; 358 } 359 360 // Anything that resolves to zero is not interpretted as a number 361 if (result == 0) return null; 362 return NameOrOrdinal{ .ordinal = result }; 363 } 364 365 /// The Win32 RC compiler uses `iswdigit` for digit detection for base 10 366 /// numbers, which means that non-ASCII digits are 'accepted' but handled 367 /// in a totally unintuitive manner, leading to arbitrary results. 368 /// 369 /// This function will return the value that such an ordinal 'would' have 370 /// if it was run through the Win32 RC compiler. This allows us to disallow 371 /// non-ASCII digits in number literals but still detect when the Win32 372 /// RC compiler would have allowed them, so that a proper warning/error 373 /// can be emitted. 374 pub fn maybeNonAsciiOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal { 375 const buf = bytes.slice; 376 const radix = 10; 377 if (buf.len > 2 and buf[0] == '0') { 378 switch (buf[1]) { 379 // We only care about base 10 numbers here 380 'x', 'X' => return null, 381 else => {}, 382 } 383 } 384 385 var i: usize = 0; 386 var result: u16 = 0; 387 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) { 388 const c = codepoint.value; 389 const digit: u16 = digit: { 390 const is_digit = (c >= '0' and c <= '9') or isNonAsciiDigit(c); 391 if (!is_digit) return null; 392 break :digit @intCast(c - '0'); 393 }; 394 395 if (result != 0) { 396 result *%= radix; 397 } 398 result +%= digit; 399 } 400 401 // Anything that resolves to zero is not interpretted as a number 402 if (result == 0) return null; 403 return NameOrOrdinal{ .ordinal = result }; 404 } 405 406 pub fn predefinedResourceType(self: NameOrOrdinal) ?RT { 407 switch (self) { 408 .ordinal => |ordinal| { 409 if (ordinal >= 256) return null; 410 switch (@as(RT, @enumFromInt(ordinal))) { 411 .ACCELERATOR, 412 .ANICURSOR, 413 .ANIICON, 414 .BITMAP, 415 .CURSOR, 416 .DIALOG, 417 .DLGINCLUDE, 418 .DLGINIT, 419 .FONT, 420 .FONTDIR, 421 .GROUP_CURSOR, 422 .GROUP_ICON, 423 .HTML, 424 .ICON, 425 .MANIFEST, 426 .MENU, 427 .MESSAGETABLE, 428 .PLUGPLAY, 429 .RCDATA, 430 .STRING, 431 .TOOLBAR, 432 .VERSION, 433 .VXD, 434 => |rt| return rt, 435 _ => return null, 436 } 437 }, 438 .name => return null, 439 } 440 } 441 442 pub fn format(self: NameOrOrdinal, w: *std.io.Writer) !void { 443 switch (self) { 444 .name => |name| { 445 try w.print("{f}", .{std.unicode.fmtUtf16Le(name)}); 446 }, 447 .ordinal => |ordinal| { 448 try w.print("{d}", .{ordinal}); 449 }, 450 } 451 } 452 453 fn formatResourceType(self: NameOrOrdinal, w: *std.io.Writer) std.io.Writer.Error!void { 454 switch (self) { 455 .name => |name| { 456 try w.print("{f}", .{std.unicode.fmtUtf16Le(name)}); 457 }, 458 .ordinal => |ordinal| { 459 if (std.enums.tagName(RT, @enumFromInt(ordinal))) |predefined_type_name| { 460 try w.print("{s}", .{predefined_type_name}); 461 } else { 462 try w.print("{d}", .{ordinal}); 463 } 464 }, 465 } 466 } 467 468 pub fn fmtResourceType(type_value: NameOrOrdinal) std.fmt.Formatter(NameOrOrdinal, formatResourceType) { 469 return .{ .data = type_value }; 470 } 471 }; 472 473 fn expectNameOrOrdinal(expected: NameOrOrdinal, actual: NameOrOrdinal) !void { 474 switch (expected) { 475 .name => { 476 if (actual != .name) return error.TestExpectedEqual; 477 try std.testing.expectEqualSlices(u16, expected.name, actual.name); 478 }, 479 .ordinal => { 480 if (actual != .ordinal) return error.TestExpectedEqual; 481 try std.testing.expectEqual(expected.ordinal, actual.ordinal); 482 }, 483 } 484 } 485 486 test "NameOrOrdinal" { 487 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 488 defer arena.deinit(); 489 490 const allocator = arena.allocator(); 491 492 // zero is treated as a string 493 try expectNameOrOrdinal( 494 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0") }, 495 try NameOrOrdinal.fromString(allocator, .{ .slice = "0", .code_page = .windows1252 }), 496 ); 497 // any non-digit byte invalidates the number 498 try expectNameOrOrdinal( 499 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1A") }, 500 try NameOrOrdinal.fromString(allocator, .{ .slice = "1a", .code_page = .windows1252 }), 501 ); 502 try expectNameOrOrdinal( 503 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1ÿ") }, 504 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\xff", .code_page = .windows1252 }), 505 ); 506 try expectNameOrOrdinal( 507 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1€") }, 508 try NameOrOrdinal.fromString(allocator, .{ .slice = "1€", .code_page = .utf8 }), 509 ); 510 try expectNameOrOrdinal( 511 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1�") }, 512 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\x80", .code_page = .utf8 }), 513 ); 514 // same with overflow that resolves to 0 515 try expectNameOrOrdinal( 516 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("65536") }, 517 try NameOrOrdinal.fromString(allocator, .{ .slice = "65536", .code_page = .windows1252 }), 518 ); 519 // hex zero is also treated as a string 520 try expectNameOrOrdinal( 521 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0X0") }, 522 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x0", .code_page = .windows1252 }), 523 ); 524 // hex numbers work 525 try expectNameOrOrdinal( 526 NameOrOrdinal{ .ordinal = 0x100 }, 527 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x100", .code_page = .windows1252 }), 528 ); 529 // only the first 4 hex digits matter 530 try expectNameOrOrdinal( 531 NameOrOrdinal{ .ordinal = 0x1234 }, 532 try NameOrOrdinal.fromString(allocator, .{ .slice = "0X12345", .code_page = .windows1252 }), 533 ); 534 // octal is not supported so it gets treated as a string 535 try expectNameOrOrdinal( 536 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0O1234") }, 537 try NameOrOrdinal.fromString(allocator, .{ .slice = "0o1234", .code_page = .windows1252 }), 538 ); 539 // overflow wraps 540 try expectNameOrOrdinal( 541 NameOrOrdinal{ .ordinal = @truncate(65635) }, 542 try NameOrOrdinal.fromString(allocator, .{ .slice = "65635", .code_page = .windows1252 }), 543 ); 544 // non-hex-digits in a hex literal are treated as a terminator 545 try expectNameOrOrdinal( 546 NameOrOrdinal{ .ordinal = 0x4 }, 547 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x4n", .code_page = .windows1252 }), 548 ); 549 try expectNameOrOrdinal( 550 NameOrOrdinal{ .ordinal = 0xFA }, 551 try NameOrOrdinal.fromString(allocator, .{ .slice = "0xFAZ92348", .code_page = .windows1252 }), 552 ); 553 // 0 at the start is allowed 554 try expectNameOrOrdinal( 555 NameOrOrdinal{ .ordinal = 50 }, 556 try NameOrOrdinal.fromString(allocator, .{ .slice = "050", .code_page = .windows1252 }), 557 ); 558 // limit of 256 UTF-16 code units, can cut off between a surrogate pair 559 { 560 var expected = blk: { 561 // the input before the 𐐷 character, but uppercased 562 const expected_u8_bytes = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528QFFL7SHNSIETG0QKLR1UYPBTUV1PMFQRRA0VJDG354GQEDJMUPGPP1W1EXVNTZVEIZ6K3IPQM1AWGEYALMEODYVEZGOD3MFMGEY8FNR4JUETTB1PZDEWSNDRGZUA8SNXP3NGO"; 563 var buf: [256:0]u16 = undefined; 564 for (expected_u8_bytes, 0..) |byte, i| { 565 buf[i] = std.mem.nativeToLittle(u16, byte); 566 } 567 // surrogate pair that is now orphaned 568 buf[255] = std.mem.nativeToLittle(u16, 0xD801); 569 break :blk buf; 570 }; 571 try expectNameOrOrdinal( 572 NameOrOrdinal{ .name = &expected }, 573 try NameOrOrdinal.fromString(allocator, .{ 574 .slice = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528qffL7ShnSIETg0qkLr1UYpbtuv1PMFQRRa0VjDG354GQedJmUPgpp1w1ExVnTzVEiz6K3iPqM1AWGeYALmeODyvEZGOD3MfmGey8fnR4jUeTtB1PzdeWsNDrGzuA8Snxp3NGO𐐷", 575 .code_page = .utf8, 576 }), 577 ); 578 } 579 } 580 581 test "NameOrOrdinal code page awareness" { 582 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 583 defer arena.deinit(); 584 585 const allocator = arena.allocator(); 586 587 try expectNameOrOrdinal( 588 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("��𐐷") }, 589 try NameOrOrdinal.fromString(allocator, .{ 590 .slice = "\xF0\x80\x80𐐷", 591 .code_page = .utf8, 592 }), 593 ); 594 try expectNameOrOrdinal( 595 // The UTF-8 representation of 𐐷 is 0xF0 0x90 0x90 0xB7. In order to provide valid 596 // UTF-8 to utf8ToUtf16LeStringLiteral, it uses the UTF-8 representation of the codepoint 597 // <U+0x90> which is 0xC2 0x90. The code units in the expected UTF-16 string are: 598 // { 0x00F0, 0x20AC, 0x20AC, 0x00F0, 0x0090, 0x0090, 0x00B7 } 599 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("ð€€ð\xC2\x90\xC2\x90·") }, 600 try NameOrOrdinal.fromString(allocator, .{ 601 .slice = "\xF0\x80\x80𐐷", 602 .code_page = .windows1252, 603 }), 604 ); 605 } 606 607 /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-accel#members 608 /// https://devblogs.microsoft.com/oldnewthing/20070316-00/?p=27593 609 pub const AcceleratorModifiers = struct { 610 value: u8 = 0, 611 explicit_ascii_or_virtkey: bool = false, 612 613 pub const ASCII = 0; 614 pub const VIRTKEY = 1; 615 pub const NOINVERT = 1 << 1; 616 pub const SHIFT = 1 << 2; 617 pub const CONTROL = 1 << 3; 618 pub const ALT = 1 << 4; 619 /// Marker for the last accelerator in an accelerator table 620 pub const last_accelerator_in_table = 1 << 7; 621 622 pub fn apply(self: *AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) void { 623 if (modifier == .ascii or modifier == .virtkey) self.explicit_ascii_or_virtkey = true; 624 self.value |= modifierValue(modifier); 625 } 626 627 pub fn isSet(self: AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) bool { 628 // ASCII is set whenever VIRTKEY is not 629 if (modifier == .ascii) return self.value & modifierValue(.virtkey) == 0; 630 return self.value & modifierValue(modifier) != 0; 631 } 632 633 fn modifierValue(modifier: rc.AcceleratorTypeAndOptions) u8 { 634 return switch (modifier) { 635 .ascii => ASCII, 636 .virtkey => VIRTKEY, 637 .noinvert => NOINVERT, 638 .shift => SHIFT, 639 .control => CONTROL, 640 .alt => ALT, 641 }; 642 } 643 644 pub fn markLast(self: *AcceleratorModifiers) void { 645 self.value |= last_accelerator_in_table; 646 } 647 }; 648 649 const AcceleratorKeyCodepointTranslator = struct { 650 string_type: literals.StringType, 651 output_code_page: SupportedCodePage, 652 653 pub fn translate(self: @This(), maybe_parsed: ?literals.IterativeStringParser.ParsedCodepoint) ?u21 { 654 const parsed = maybe_parsed orelse return null; 655 if (parsed.codepoint == Codepoint.invalid) return 0xFFFD; 656 if (parsed.from_escaped_integer) { 657 switch (self.string_type) { 658 .ascii => { 659 const truncated: u8 = @truncate(parsed.codepoint); 660 switch (self.output_code_page) { 661 .utf8 => switch (truncated) { 662 0...0x7F => return truncated, 663 else => return 0xFFFD, 664 }, 665 .windows1252 => return windows1252.toCodepoint(truncated), 666 } 667 }, 668 .wide => { 669 const truncated: u16 = @truncate(parsed.codepoint); 670 return truncated; 671 }, 672 } 673 } 674 if (parsed.escaped_surrogate_pair) { 675 // The codepoint of only the low surrogate 676 const low = @as(u16, @intCast(parsed.codepoint & 0x3FF)) + 0xDC00; 677 return low; 678 } 679 return parsed.codepoint; 680 } 681 }; 682 683 pub const ParseAcceleratorKeyStringError = error{ EmptyAccelerator, AcceleratorTooLong, InvalidControlCharacter, ControlCharacterOutOfRange }; 684 685 /// Expects bytes to be the full bytes of a string literal token (e.g. including the "" or L""). 686 pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: literals.StringParseOptions) (ParseAcceleratorKeyStringError || Allocator.Error)!u16 { 687 if (bytes.slice.len == 0) { 688 return error.EmptyAccelerator; 689 } 690 691 var parser = literals.IterativeStringParser.init(bytes, options); 692 var translator = AcceleratorKeyCodepointTranslator{ 693 .string_type = parser.declared_string_type, 694 .output_code_page = options.output_code_page, 695 }; 696 697 const first_codepoint = translator.translate(try parser.next()) orelse return error.EmptyAccelerator; 698 // 0 is treated as a terminator, so this is equivalent to an empty string 699 if (first_codepoint == 0) return error.EmptyAccelerator; 700 701 if (first_codepoint == '^') { 702 // Note: Emitting this warning unconditionally whenever ^ is the first character 703 // matches the Win32 RC behavior, but it's questionable whether or not 704 // the warning should be emitted for ^^ since that results in the ASCII 705 // character ^ being written to the .res. 706 if (is_virt and options.diagnostics != null) { 707 try options.diagnostics.?.diagnostics.append(.{ 708 .err = .ascii_character_not_equivalent_to_virtual_key_code, 709 .type = .warning, 710 .code_page = bytes.code_page, 711 .token = options.diagnostics.?.token, 712 }); 713 } 714 715 const c = translator.translate(try parser.next()) orelse return error.InvalidControlCharacter; 716 717 const third_codepoint = translator.translate(try parser.next()); 718 // 0 is treated as a terminator, so a 0 in the third position is fine but 719 // anything else is too many codepoints for an accelerator 720 if (third_codepoint != null and third_codepoint.? != 0) return error.InvalidControlCharacter; 721 722 switch (c) { 723 '^' => return '^', // special case 724 'a'...'z', 'A'...'Z' => return std.ascii.toUpper(@intCast(c)) - 0x40, 725 // Note: The Windows RC compiler allows more than just A-Z, but what it allows 726 // seems to be tied to some sort of Unicode-aware 'is character' function or something. 727 // The full list of codepoints that trigger an out-of-range error can be found here: 728 // https://gist.github.com/squeek502/2e9d0a4728a83eed074ad9785a209fd0 729 // For codepoints >= 0x80 that don't trigger the error, the Windows RC compiler takes the 730 // codepoint and does the `- 0x40` transformation as if it were A-Z which couldn't lead 731 // to anything useable, so there's no point in emulating that behavior--erroring for 732 // all non-[a-zA-Z] makes much more sense and is what was probably intended by the 733 // Windows RC compiler. 734 else => return error.ControlCharacterOutOfRange, 735 } 736 @compileError("this should be unreachable"); 737 } 738 739 const second_codepoint = translator.translate(try parser.next()); 740 741 var result: u32 = initial_value: { 742 if (first_codepoint >= 0x10000) { 743 if (second_codepoint != null and second_codepoint.? != 0) return error.AcceleratorTooLong; 744 // No idea why it works this way, but this seems to match the Windows RC 745 // behavior for codepoints >= 0x10000 746 const low = @as(u16, @intCast(first_codepoint & 0x3FF)) + 0xDC00; 747 const extra = (first_codepoint - 0x10000) / 0x400; 748 break :initial_value low + extra * 0x100; 749 } 750 break :initial_value first_codepoint; 751 }; 752 753 // 0 is treated as a terminator 754 if (second_codepoint != null and second_codepoint.? == 0) return @truncate(result); 755 756 const third_codepoint = translator.translate(try parser.next()); 757 // 0 is treated as a terminator, so a 0 in the third position is fine but 758 // anything else is too many codepoints for an accelerator 759 if (third_codepoint != null and third_codepoint.? != 0) return error.AcceleratorTooLong; 760 761 if (second_codepoint) |c| { 762 if (c >= 0x10000) return error.AcceleratorTooLong; 763 result <<= 8; 764 result += c; 765 } else if (is_virt) { 766 switch (result) { 767 'a'...'z' => result -= 0x20, // toUpper 768 else => {}, 769 } 770 } 771 return @truncate(result); 772 } 773 774 test "accelerator keys" { 775 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString( 776 .{ .slice = "\"^a\"", .code_page = .windows1252 }, 777 false, 778 .{ .output_code_page = .windows1252 }, 779 )); 780 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString( 781 .{ .slice = "\"^A\"", .code_page = .windows1252 }, 782 false, 783 .{ .output_code_page = .windows1252 }, 784 )); 785 try std.testing.expectEqual(@as(u16, 26), try parseAcceleratorKeyString( 786 .{ .slice = "\"^Z\"", .code_page = .windows1252 }, 787 false, 788 .{ .output_code_page = .windows1252 }, 789 )); 790 try std.testing.expectEqual(@as(u16, '^'), try parseAcceleratorKeyString( 791 .{ .slice = "\"^^\"", .code_page = .windows1252 }, 792 false, 793 .{ .output_code_page = .windows1252 }, 794 )); 795 796 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString( 797 .{ .slice = "\"a\"", .code_page = .windows1252 }, 798 false, 799 .{ .output_code_page = .windows1252 }, 800 )); 801 try std.testing.expectEqual(@as(u16, 0x6162), try parseAcceleratorKeyString( 802 .{ .slice = "\"ab\"", .code_page = .windows1252 }, 803 false, 804 .{ .output_code_page = .windows1252 }, 805 )); 806 807 try std.testing.expectEqual(@as(u16, 'C'), try parseAcceleratorKeyString( 808 .{ .slice = "\"c\"", .code_page = .windows1252 }, 809 true, 810 .{ .output_code_page = .windows1252 }, 811 )); 812 try std.testing.expectEqual(@as(u16, 0x6363), try parseAcceleratorKeyString( 813 .{ .slice = "\"cc\"", .code_page = .windows1252 }, 814 true, 815 .{ .output_code_page = .windows1252 }, 816 )); 817 818 // \x00 or any escape that evaluates to zero acts as a terminator, everything past it 819 // is ignored 820 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString( 821 .{ .slice = "\"a\\0bcdef\"", .code_page = .windows1252 }, 822 false, 823 .{ .output_code_page = .windows1252 }, 824 )); 825 826 // \x80 is € in Windows-1252, which is Unicode codepoint 20AC 827 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString( 828 .{ .slice = "\"\x80\"", .code_page = .windows1252 }, 829 false, 830 .{ .output_code_page = .windows1252 }, 831 )); 832 // This depends on the code page, though, with codepage 65001, \x80 833 // on its own is invalid UTF-8 so it gets converted to the replacement character 834 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString( 835 .{ .slice = "\"\x80\"", .code_page = .utf8 }, 836 false, 837 .{ .output_code_page = .windows1252 }, 838 )); 839 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString( 840 .{ .slice = "\"\x80\x80\"", .code_page = .windows1252 }, 841 false, 842 .{ .output_code_page = .windows1252 }, 843 )); 844 // This also behaves the same with escaped characters 845 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString( 846 .{ .slice = "\"\\x80\"", .code_page = .windows1252 }, 847 false, 848 .{ .output_code_page = .windows1252 }, 849 )); 850 // Even with utf8 code page 851 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString( 852 .{ .slice = "\"\\x80\"", .code_page = .utf8 }, 853 false, 854 .{ .output_code_page = .windows1252 }, 855 )); 856 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString( 857 .{ .slice = "\"\\x80\\x80\"", .code_page = .windows1252 }, 858 false, 859 .{ .output_code_page = .windows1252 }, 860 )); 861 // Wide string with the actual characters behaves like the ASCII string version 862 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString( 863 .{ .slice = "L\"\x80\x80\"", .code_page = .windows1252 }, 864 false, 865 .{ .output_code_page = .windows1252 }, 866 )); 867 // But wide string with escapes behaves differently 868 try std.testing.expectEqual(@as(u16, 0x8080), try parseAcceleratorKeyString( 869 .{ .slice = "L\"\\x80\\x80\"", .code_page = .windows1252 }, 870 false, 871 .{ .output_code_page = .windows1252 }, 872 )); 873 // and invalid escapes within wide strings get skipped 874 try std.testing.expectEqual(@as(u16, 'z'), try parseAcceleratorKeyString( 875 .{ .slice = "L\"\\Hz\"", .code_page = .windows1252 }, 876 false, 877 .{ .output_code_page = .windows1252 }, 878 )); 879 880 // any non-A-Z codepoints are illegal 881 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString( 882 .{ .slice = "\"^\x83\"", .code_page = .windows1252 }, 883 false, 884 .{ .output_code_page = .windows1252 }, 885 )); 886 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString( 887 .{ .slice = "\"^1\"", .code_page = .windows1252 }, 888 false, 889 .{ .output_code_page = .windows1252 }, 890 )); 891 try std.testing.expectError(error.InvalidControlCharacter, parseAcceleratorKeyString( 892 .{ .slice = "\"^\"", .code_page = .windows1252 }, 893 false, 894 .{ .output_code_page = .windows1252 }, 895 )); 896 try std.testing.expectError(error.EmptyAccelerator, parseAcceleratorKeyString( 897 .{ .slice = "\"\"", .code_page = .windows1252 }, 898 false, 899 .{ .output_code_page = .windows1252 }, 900 )); 901 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString( 902 .{ .slice = "\"hello\"", .code_page = .windows1252 }, 903 false, 904 .{ .output_code_page = .windows1252 }, 905 )); 906 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString( 907 .{ .slice = "\"^\x80\"", .code_page = .windows1252 }, 908 false, 909 .{ .output_code_page = .windows1252 }, 910 )); 911 912 // Invalid UTF-8 gets converted to 0xFFFD, multiple invalids get shifted and added together 913 // The behavior is the same for ascii and wide strings 914 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString( 915 .{ .slice = "\"\x80\x80\"", .code_page = .utf8 }, 916 false, 917 .{ .output_code_page = .windows1252 }, 918 )); 919 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString( 920 .{ .slice = "L\"\x80\x80\"", .code_page = .utf8 }, 921 false, 922 .{ .output_code_page = .windows1252 }, 923 )); 924 925 // Codepoints >= 0x10000 926 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString( 927 .{ .slice = "\"\xF0\x90\x84\x80\"", .code_page = .utf8 }, 928 false, 929 .{ .output_code_page = .windows1252 }, 930 )); 931 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString( 932 .{ .slice = "L\"\xF0\x90\x84\x80\"", .code_page = .utf8 }, 933 false, 934 .{ .output_code_page = .windows1252 }, 935 )); 936 try std.testing.expectEqual(@as(u16, 0x9C01), try parseAcceleratorKeyString( 937 .{ .slice = "\"\xF4\x80\x80\x81\"", .code_page = .utf8 }, 938 false, 939 .{ .output_code_page = .windows1252 }, 940 )); 941 // anything before or after a codepoint >= 0x10000 causes an error 942 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString( 943 .{ .slice = "\"a\xF0\x90\x80\x80\"", .code_page = .utf8 }, 944 false, 945 .{ .output_code_page = .windows1252 }, 946 )); 947 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString( 948 .{ .slice = "\"\xF0\x90\x80\x80a\"", .code_page = .utf8 }, 949 false, 950 .{ .output_code_page = .windows1252 }, 951 )); 952 953 // Misc special cases 954 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString( 955 .{ .slice = "\"\\777\"", .code_page = .utf8 }, 956 false, 957 .{ .output_code_page = .utf8 }, 958 )); 959 try std.testing.expectEqual(@as(u16, 0xFFFF), try parseAcceleratorKeyString( 960 .{ .slice = "L\"\\7777777\"", .code_page = .utf8 }, 961 false, 962 .{ .output_code_page = .utf8 }, 963 )); 964 try std.testing.expectEqual(@as(u16, 0x01), try parseAcceleratorKeyString( 965 .{ .slice = "L\"\\200001\"", .code_page = .utf8 }, 966 false, 967 .{ .output_code_page = .utf8 }, 968 )); 969 // Escape of a codepoint >= 0x10000 omits the high surrogate pair 970 try std.testing.expectEqual(@as(u16, 0xDF48), try parseAcceleratorKeyString( 971 .{ .slice = "L\"\\𐍈\"", .code_page = .utf8 }, 972 false, 973 .{ .output_code_page = .utf8 }, 974 )); 975 // Invalid escape code is skipped, allows for 2 codepoints afterwards 976 try std.testing.expectEqual(@as(u16, 0x7878), try parseAcceleratorKeyString( 977 .{ .slice = "L\"\\kxx\"", .code_page = .utf8 }, 978 false, 979 .{ .output_code_page = .utf8 }, 980 )); 981 // Escape of a codepoint >= 0x10000 allows for a codepoint afterwards 982 try std.testing.expectEqual(@as(u16, 0x4878), try parseAcceleratorKeyString( 983 .{ .slice = "L\"\\𐍈x\"", .code_page = .utf8 }, 984 false, 985 .{ .output_code_page = .utf8 }, 986 )); 987 // Input code page of 1252, output code page of utf-8 988 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString( 989 .{ .slice = "\"\\270\"", .code_page = .windows1252 }, 990 false, 991 .{ .output_code_page = .utf8 }, 992 )); 993 } 994 995 pub const ForcedOrdinal = struct { 996 pub fn fromBytes(bytes: SourceBytes) u16 { 997 var i: usize = 0; 998 var result: u21 = 0; 999 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) { 1000 const c = switch (codepoint.value) { 1001 // Codepoints that would need a surrogate pair in UTF-16 are 1002 // broken up into their UTF-16 code units and each code unit 1003 // is interpreted as a digit. 1004 0x10000...0x10FFFF => { 1005 const high = @as(u16, @intCast((codepoint.value - 0x10000) >> 10)) + 0xD800; 1006 if (result != 0) result *%= 10; 1007 result +%= high -% '0'; 1008 1009 const low = @as(u16, @intCast(codepoint.value & 0x3FF)) + 0xDC00; 1010 if (result != 0) result *%= 10; 1011 result +%= low -% '0'; 1012 continue; 1013 }, 1014 Codepoint.invalid => 0xFFFD, 1015 else => codepoint.value, 1016 }; 1017 if (result != 0) result *%= 10; 1018 result +%= c -% '0'; 1019 } 1020 return @truncate(result); 1021 } 1022 1023 pub fn fromUtf16Le(utf16: [:0]const u16) u16 { 1024 var result: u16 = 0; 1025 for (utf16) |code_unit| { 1026 if (result != 0) result *%= 10; 1027 result +%= std.mem.littleToNative(u16, code_unit) -% '0'; 1028 } 1029 return result; 1030 } 1031 }; 1032 1033 test "forced ordinal" { 1034 try std.testing.expectEqual(@as(u16, 3200), ForcedOrdinal.fromBytes(.{ .slice = "3200", .code_page = .windows1252 })); 1035 try std.testing.expectEqual(@as(u16, 0x33), ForcedOrdinal.fromBytes(.{ .slice = "1+1", .code_page = .windows1252 })); 1036 try std.testing.expectEqual(@as(u16, 65531), ForcedOrdinal.fromBytes(.{ .slice = "1!", .code_page = .windows1252 })); 1037 1038 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0\x8C", .code_page = .windows1252 })); 1039 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0Œ", .code_page = .utf8 })); 1040 1041 // invalid UTF-8 gets converted to 0xFFFD (replacement char) and then interpreted as a digit 1042 try std.testing.expectEqual(@as(u16, 0xFFCD), ForcedOrdinal.fromBytes(.{ .slice = "0\x81", .code_page = .utf8 })); 1043 // codepoints >= 0x10000 1044 try std.testing.expectEqual(@as(u16, 0x49F2), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10002}", .code_page = .utf8 })); 1045 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10100}", .code_page = .utf8 })); 1046 1047 // From UTF-16 1048 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromUtf16Le(&[_:0]u16{ std.mem.nativeToLittle(u16, '0'), std.mem.nativeToLittle(u16, 'Œ') })); 1049 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromUtf16Le(std.unicode.utf8ToUtf16LeStringLiteral("0\u{10100}"))); 1050 } 1051 1052 /// https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo 1053 pub const FixedFileInfo = struct { 1054 file_version: Version = .{}, 1055 product_version: Version = .{}, 1056 file_flags_mask: u32 = 0, 1057 file_flags: u32 = 0, 1058 file_os: u32 = 0, 1059 file_type: u32 = 0, 1060 file_subtype: u32 = 0, 1061 file_date: Version = .{}, // TODO: I think this is always all zeroes? 1062 1063 pub const signature = 0xFEEF04BD; 1064 // Note: This corresponds to a version of 1.0 1065 pub const version = 0x00010000; 1066 1067 pub const byte_len = 0x34; 1068 pub const key = std.unicode.utf8ToUtf16LeStringLiteral("VS_VERSION_INFO"); 1069 1070 pub const Version = struct { 1071 parts: [4]u16 = [_]u16{0} ** 4, 1072 1073 pub fn mostSignificantCombinedParts(self: Version) u32 { 1074 return (@as(u32, self.parts[0]) << 16) + self.parts[1]; 1075 } 1076 1077 pub fn leastSignificantCombinedParts(self: Version) u32 { 1078 return (@as(u32, self.parts[2]) << 16) + self.parts[3]; 1079 } 1080 }; 1081 1082 pub fn write(self: FixedFileInfo, writer: anytype) !void { 1083 try writer.writeInt(u32, signature, .little); 1084 try writer.writeInt(u32, version, .little); 1085 try writer.writeInt(u32, self.file_version.mostSignificantCombinedParts(), .little); 1086 try writer.writeInt(u32, self.file_version.leastSignificantCombinedParts(), .little); 1087 try writer.writeInt(u32, self.product_version.mostSignificantCombinedParts(), .little); 1088 try writer.writeInt(u32, self.product_version.leastSignificantCombinedParts(), .little); 1089 try writer.writeInt(u32, self.file_flags_mask, .little); 1090 try writer.writeInt(u32, self.file_flags, .little); 1091 try writer.writeInt(u32, self.file_os, .little); 1092 try writer.writeInt(u32, self.file_type, .little); 1093 try writer.writeInt(u32, self.file_subtype, .little); 1094 try writer.writeInt(u32, self.file_date.mostSignificantCombinedParts(), .little); 1095 try writer.writeInt(u32, self.file_date.leastSignificantCombinedParts(), .little); 1096 } 1097 }; 1098 1099 test "FixedFileInfo.Version" { 1100 const version = FixedFileInfo.Version{ 1101 .parts = .{ 1, 2, 3, 4 }, 1102 }; 1103 try std.testing.expectEqual(@as(u32, 0x00010002), version.mostSignificantCombinedParts()); 1104 try std.testing.expectEqual(@as(u32, 0x00030004), version.leastSignificantCombinedParts()); 1105 } 1106 1107 pub const VersionNode = struct { 1108 pub const type_string: u16 = 1; 1109 pub const type_binary: u16 = 0; 1110 }; 1111 1112 pub const MenuItemFlags = struct { 1113 value: u16 = 0, 1114 1115 pub fn apply(self: *MenuItemFlags, option: rc.MenuItem.Option) void { 1116 self.value |= optionValue(option); 1117 } 1118 1119 pub fn isSet(self: MenuItemFlags, option: rc.MenuItem.Option) bool { 1120 return self.value & optionValue(option) != 0; 1121 } 1122 1123 fn optionValue(option: rc.MenuItem.Option) u16 { 1124 return @intCast(switch (option) { 1125 .checked => MF.CHECKED, 1126 .grayed => MF.GRAYED, 1127 .help => MF.HELP, 1128 .inactive => MF.DISABLED, 1129 .menubarbreak => MF.MENUBARBREAK, 1130 .menubreak => MF.MENUBREAK, 1131 }); 1132 } 1133 1134 pub fn markLast(self: *MenuItemFlags) void { 1135 self.value |= @intCast(MF.END); 1136 } 1137 }; 1138 1139 /// Menu Flags from WinUser.h 1140 /// This is not complete, it only contains what is needed 1141 pub const MF = struct { 1142 pub const GRAYED: u32 = 0x00000001; 1143 pub const DISABLED: u32 = 0x00000002; 1144 pub const CHECKED: u32 = 0x00000008; 1145 pub const POPUP: u32 = 0x00000010; 1146 pub const MENUBARBREAK: u32 = 0x00000020; 1147 pub const MENUBREAK: u32 = 0x00000040; 1148 pub const HELP: u32 = 0x00004000; 1149 pub const END: u32 = 0x00000080; 1150 }; 1151 1152 /// Window Styles from WinUser.h 1153 pub const WS = struct { 1154 pub const OVERLAPPED: u32 = 0x00000000; 1155 pub const POPUP: u32 = 0x80000000; 1156 pub const CHILD: u32 = 0x40000000; 1157 pub const MINIMIZE: u32 = 0x20000000; 1158 pub const VISIBLE: u32 = 0x10000000; 1159 pub const DISABLED: u32 = 0x08000000; 1160 pub const CLIPSIBLINGS: u32 = 0x04000000; 1161 pub const CLIPCHILDREN: u32 = 0x02000000; 1162 pub const MAXIMIZE: u32 = 0x01000000; 1163 pub const CAPTION: u32 = BORDER | DLGFRAME; 1164 pub const BORDER: u32 = 0x00800000; 1165 pub const DLGFRAME: u32 = 0x00400000; 1166 pub const VSCROLL: u32 = 0x00200000; 1167 pub const HSCROLL: u32 = 0x00100000; 1168 pub const SYSMENU: u32 = 0x00080000; 1169 pub const THICKFRAME: u32 = 0x00040000; 1170 pub const GROUP: u32 = 0x00020000; 1171 pub const TABSTOP: u32 = 0x00010000; 1172 1173 pub const MINIMIZEBOX: u32 = 0x00020000; 1174 pub const MAXIMIZEBOX: u32 = 0x00010000; 1175 1176 pub const TILED: u32 = OVERLAPPED; 1177 pub const ICONIC: u32 = MINIMIZE; 1178 pub const SIZEBOX: u32 = THICKFRAME; 1179 pub const TILEDWINDOW: u32 = OVERLAPPEDWINDOW; 1180 1181 // Common Window Styles 1182 pub const OVERLAPPEDWINDOW: u32 = OVERLAPPED | CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX; 1183 pub const POPUPWINDOW: u32 = POPUP | BORDER | SYSMENU; 1184 pub const CHILDWINDOW: u32 = CHILD; 1185 }; 1186 1187 /// Dialog Box Template Styles from WinUser.h 1188 pub const DS = struct { 1189 pub const SETFONT: u32 = 0x40; 1190 }; 1191 1192 /// Button Control Styles from WinUser.h 1193 /// This is not complete, it only contains what is needed 1194 pub const BS = struct { 1195 pub const PUSHBUTTON: u32 = 0x00000000; 1196 pub const DEFPUSHBUTTON: u32 = 0x00000001; 1197 pub const CHECKBOX: u32 = 0x00000002; 1198 pub const AUTOCHECKBOX: u32 = 0x00000003; 1199 pub const RADIOBUTTON: u32 = 0x00000004; 1200 pub const @"3STATE": u32 = 0x00000005; 1201 pub const AUTO3STATE: u32 = 0x00000006; 1202 pub const GROUPBOX: u32 = 0x00000007; 1203 pub const USERBUTTON: u32 = 0x00000008; 1204 pub const AUTORADIOBUTTON: u32 = 0x00000009; 1205 pub const PUSHBOX: u32 = 0x0000000A; 1206 pub const OWNERDRAW: u32 = 0x0000000B; 1207 pub const TYPEMASK: u32 = 0x0000000F; 1208 pub const LEFTTEXT: u32 = 0x00000020; 1209 }; 1210 1211 /// Static Control Constants from WinUser.h 1212 /// This is not complete, it only contains what is needed 1213 pub const SS = struct { 1214 pub const LEFT: u32 = 0x00000000; 1215 pub const CENTER: u32 = 0x00000001; 1216 pub const RIGHT: u32 = 0x00000002; 1217 pub const ICON: u32 = 0x00000003; 1218 }; 1219 1220 /// Listbox Styles from WinUser.h 1221 /// This is not complete, it only contains what is needed 1222 pub const LBS = struct { 1223 pub const NOTIFY: u32 = 0x0001; 1224 };