master
1const std = @import("std");
2const assert = std.debug.assert;
3const rc = @import("rc.zig");
4const ResourceType = rc.ResourceType;
5const CommonResourceAttributes = rc.CommonResourceAttributes;
6const Allocator = std.mem.Allocator;
7const windows1252 = @import("windows1252.zig");
8const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
9const literals = @import("literals.zig");
10const SourceBytes = literals.SourceBytes;
11const Codepoint = @import("code_pages.zig").Codepoint;
12const lang = @import("lang.zig");
13const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
14
15/// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
16pub 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
80pub 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
149pub 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
183pub 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
235pub 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: *std.Io.Writer) !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: *std.Io.Writer) !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.ArrayList(u16).initCapacity(allocator, @min(257, bytes.slice.len));
287 errdefer buf.deinit(allocator);
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(allocator, std.mem.nativeToLittle(u16, '�'));
296 } else if (c < 0x7F) {
297 // ASCII chars in names are always converted to uppercase
298 try buf.append(allocator, std.mem.nativeToLittle(u16, std.ascii.toUpper(@intCast(c))));
299 } else if (c < 0x10000) {
300 const short: u16 = @intCast(c);
301 try buf.append(allocator, std.mem.nativeToLittle(u16, short));
302 } else {
303 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
304 try buf.append(allocator, 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(allocator, std.mem.nativeToLittle(u16, low));
312 }
313 }
314
315 return NameOrOrdinal{ .name = try buf.toOwnedSliceSentinel(allocator, 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.Alt(NameOrOrdinal, formatResourceType) {
469 return .{ .data = type_value };
470 }
471};
472
473fn 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
486test "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
581test "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
609pub 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
649const 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
683pub 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"").
686pub 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
774test "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
995pub 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
1033test "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
1053pub 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: *std.Io.Writer) !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
1099test "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
1107pub const VersionNode = struct {
1108 pub const type_string: u16 = 1;
1109 pub const type_binary: u16 = 0;
1110};
1111
1112pub 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
1141pub 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
1153pub 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
1188pub 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
1194pub 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
1213pub 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
1222pub const LBS = struct {
1223 pub const NOTIFY: u32 = 0x0001;
1224};