authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2024-03-02 00:50:53-08:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2024-03-11 05:06:16-07:00
log52de2802c457140f3d9923cf014b51bb8c16689f
treeed2accf0d3b85155e01ea7063792f28ec3d7184b
parentd0c06ca7127110a8afeb0ef524a197049892db21

Lazily compile the `zig rc` subcommand and use it during `zig build-exe`

This moves .rc/.manifest compilation out of the main Zig binary, contributing towards #19063 Also: - Make resinator use Aro as its preprocessor instead of clang - Sync resinator with upstream

42 files changed, 17002 insertions(+), 16939 deletions(-)

lib/compiler/resinator/ani.zig created+58
...@@ -0,0 +1,58 @@
1//! https://en.wikipedia.org/wiki/Resource_Interchange_File_Format
2//! https://www.moon-soft.com/program/format/windows/ani.htm
3//! https://www.gdgsoft.com/anituner/help/aniformat.htm
4//! https://www.lomont.org/software/aniexploit/ExploitANI.pdf
5//!
6//! RIFF( 'ACON'
7//! [LIST( 'INFO' <info_data> )]
8//! [<DISP_ck>]
9//! anih( <ani_header> )
10//! [rate( <rate_info> )]
11//! ['seq '( <sequence_info> )]
12//! LIST( 'fram' icon( <icon_file> ) ... )
13//! )
14
15const std = @import("std");
16
17const AF_ICON: u32 = 1;
18
19pub fn isAnimatedIcon(reader: anytype) bool {
20 const flags = getAniheaderFlags(reader) catch return false;
21 return flags & AF_ICON == AF_ICON;
22}
23
24fn getAniheaderFlags(reader: anytype) !u32 {
25 const riff_header = try reader.readBytesNoEof(4);
26 if (!std.mem.eql(u8, &riff_header, "RIFF")) return error.InvalidFormat;
27
28 _ = try reader.readInt(u32, .little); // 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.readInt(u32, .little);
36 if (!std.mem.eql(u8, &chunk_id, "anih")) {
37 // TODO: Move file cursor instead of skipBytes
38 try reader.skipBytes(chunk_len, .{});
39 continue;
40 }
41
42 const aniheader = try reader.readStruct(ANIHEADER);
43 return std.mem.nativeToLittle(u32, aniheader.flags);
44 }
45}
46
47/// From Microsoft Multimedia Data Standards Update April 15, 1994
48const ANIHEADER = extern struct {
49 cbSizeof: u32,
50 cFrames: u32,
51 cSteps: u32,
52 cx: u32,
53 cy: u32,
54 cBitCount: u32,
55 cPlanes: u32,
56 jifRate: u32,
57 flags: u32,
58};
lib/compiler/resinator/ast.zig created+1084
...@@ -0,0 +1,1084 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const Token = @import("lex.zig").Token;
4const CodePage = @import("code_pages.zig").CodePage;
5
6pub const Tree = struct {
7 node: *Node,
8 input_code_pages: CodePageLookup,
9 output_code_pages: CodePageLookup,
10
11 /// not owned by the tree
12 source: []const u8,
13
14 arena: std.heap.ArenaAllocator.State,
15 allocator: Allocator,
16
17 pub fn deinit(self: *Tree) void {
18 self.arena.promote(self.allocator).deinit();
19 }
20
21 pub fn root(self: *Tree) *Node.Root {
22 return @fieldParentPtr(Node.Root, "base", self.node);
23 }
24
25 pub fn dump(self: *Tree, writer: anytype) @TypeOf(writer).Error!void {
26 try self.node.dump(self, writer, 0);
27 }
28};
29
30pub const CodePageLookup = struct {
31 lookup: std.ArrayListUnmanaged(CodePage) = .{},
32 allocator: Allocator,
33 default_code_page: CodePage,
34
35 pub fn init(allocator: Allocator, default_code_page: CodePage) CodePageLookup {
36 return .{
37 .allocator = allocator,
38 .default_code_page = default_code_page,
39 };
40 }
41
42 pub fn deinit(self: *CodePageLookup) void {
43 self.lookup.deinit(self.allocator);
44 }
45
46 /// line_num is 1-indexed
47 pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: CodePage) !void {
48 const index = line_num - 1;
49 if (index >= self.lookup.items.len) {
50 const new_size = line_num;
51 const missing_lines_start_index = self.lookup.items.len;
52 try self.lookup.resize(self.allocator, new_size);
53
54 // If there are any gaps created, we need to fill them in with the value of the
55 // last line before the gap. This can happen for e.g. string literals that
56 // span multiple lines, or if the start of a file has multiple empty lines.
57 const fill_value = if (missing_lines_start_index > 0)
58 self.lookup.items[missing_lines_start_index - 1]
59 else
60 self.default_code_page;
61 var i: usize = missing_lines_start_index;
62 while (i < new_size - 1) : (i += 1) {
63 self.lookup.items[i] = fill_value;
64 }
65 }
66 self.lookup.items[index] = code_page;
67 }
68
69 pub fn setForToken(self: *CodePageLookup, token: Token, code_page: CodePage) !void {
70 return self.setForLineNum(token.line_number, code_page);
71 }
72
73 /// line_num is 1-indexed
74 pub fn getForLineNum(self: CodePageLookup, line_num: usize) CodePage {
75 return self.lookup.items[line_num - 1];
76 }
77
78 pub fn getForToken(self: CodePageLookup, token: Token) CodePage {
79 return self.getForLineNum(token.line_number);
80 }
81};
82
83test "CodePageLookup" {
84 var lookup = CodePageLookup.init(std.testing.allocator, .windows1252);
85 defer lookup.deinit();
86
87 try lookup.setForLineNum(5, .utf8);
88 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));
89 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));
90 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));
91 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));
92 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));
93 try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len);
94
95 try lookup.setForLineNum(7, .windows1252);
96 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));
97 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));
98 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));
99 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));
100 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));
101 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(6));
102 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(7));
103 try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len);
104}
105
106pub const Node = struct {
107 id: Id,
108
109 pub const Id = enum {
110 root,
111 resource_external,
112 resource_raw_data,
113 literal,
114 binary_expression,
115 grouped_expression,
116 not_expression,
117 accelerators,
118 accelerator,
119 dialog,
120 control_statement,
121 toolbar,
122 menu,
123 menu_item,
124 menu_item_separator,
125 menu_item_ex,
126 popup,
127 popup_ex,
128 version_info,
129 version_statement,
130 block,
131 block_value,
132 block_value_value,
133 string_table,
134 string_table_string,
135 language_statement,
136 font_statement,
137 simple_statement,
138 invalid,
139
140 pub fn Type(comptime id: Id) type {
141 return switch (id) {
142 .root => Root,
143 .resource_external => ResourceExternal,
144 .resource_raw_data => ResourceRawData,
145 .literal => Literal,
146 .binary_expression => BinaryExpression,
147 .grouped_expression => GroupedExpression,
148 .not_expression => NotExpression,
149 .accelerators => Accelerators,
150 .accelerator => Accelerator,
151 .dialog => Dialog,
152 .control_statement => ControlStatement,
153 .toolbar => Toolbar,
154 .menu => Menu,
155 .menu_item => MenuItem,
156 .menu_item_separator => MenuItemSeparator,
157 .menu_item_ex => MenuItemEx,
158 .popup => Popup,
159 .popup_ex => PopupEx,
160 .version_info => VersionInfo,
161 .version_statement => VersionStatement,
162 .block => Block,
163 .block_value => BlockValue,
164 .block_value_value => BlockValueValue,
165 .string_table => StringTable,
166 .string_table_string => StringTableString,
167 .language_statement => LanguageStatement,
168 .font_statement => FontStatement,
169 .simple_statement => SimpleStatement,
170 .invalid => Invalid,
171 };
172 }
173 };
174
175 pub fn cast(base: *Node, comptime id: Id) ?*id.Type() {
176 if (base.id == id) {
177 return @fieldParentPtr(id.Type(), "base", base);
178 }
179 return null;
180 }
181
182 pub const Root = struct {
183 base: Node = .{ .id = .root },
184 body: []*Node,
185 };
186
187 pub const ResourceExternal = struct {
188 base: Node = .{ .id = .resource_external },
189 id: Token,
190 type: Token,
191 common_resource_attributes: []Token,
192 filename: *Node,
193 };
194
195 pub const ResourceRawData = struct {
196 base: Node = .{ .id = .resource_raw_data },
197 id: Token,
198 type: Token,
199 common_resource_attributes: []Token,
200 begin_token: Token,
201 raw_data: []*Node,
202 end_token: Token,
203 };
204
205 pub const Literal = struct {
206 base: Node = .{ .id = .literal },
207 token: Token,
208 };
209
210 pub const BinaryExpression = struct {
211 base: Node = .{ .id = .binary_expression },
212 operator: Token,
213 left: *Node,
214 right: *Node,
215 };
216
217 pub const GroupedExpression = struct {
218 base: Node = .{ .id = .grouped_expression },
219 open_token: Token,
220 expression: *Node,
221 close_token: Token,
222 };
223
224 pub const NotExpression = struct {
225 base: Node = .{ .id = .not_expression },
226 not_token: Token,
227 number_token: Token,
228 };
229
230 pub const Accelerators = struct {
231 base: Node = .{ .id = .accelerators },
232 id: Token,
233 type: Token,
234 common_resource_attributes: []Token,
235 optional_statements: []*Node,
236 begin_token: Token,
237 accelerators: []*Node,
238 end_token: Token,
239 };
240
241 pub const Accelerator = struct {
242 base: Node = .{ .id = .accelerator },
243 event: *Node,
244 idvalue: *Node,
245 type_and_options: []Token,
246 };
247
248 pub const Dialog = struct {
249 base: Node = .{ .id = .dialog },
250 id: Token,
251 type: Token,
252 common_resource_attributes: []Token,
253 x: *Node,
254 y: *Node,
255 width: *Node,
256 height: *Node,
257 help_id: ?*Node,
258 optional_statements: []*Node,
259 begin_token: Token,
260 controls: []*Node,
261 end_token: Token,
262 };
263
264 pub const ControlStatement = struct {
265 base: Node = .{ .id = .control_statement },
266 type: Token,
267 text: ?Token,
268 /// Only relevant for the user-defined CONTROL control
269 class: ?*Node,
270 id: *Node,
271 x: *Node,
272 y: *Node,
273 width: *Node,
274 height: *Node,
275 style: ?*Node,
276 exstyle: ?*Node,
277 help_id: ?*Node,
278 extra_data_begin: ?Token,
279 extra_data: []*Node,
280 extra_data_end: ?Token,
281
282 /// Returns true if this node describes a user-defined CONTROL control
283 /// https://learn.microsoft.com/en-us/windows/win32/menurc/control-control
284 pub fn isUserDefined(self: *const ControlStatement) bool {
285 return self.class != null;
286 }
287 };
288
289 pub const Toolbar = struct {
290 base: Node = .{ .id = .toolbar },
291 id: Token,
292 type: Token,
293 common_resource_attributes: []Token,
294 button_width: *Node,
295 button_height: *Node,
296 begin_token: Token,
297 /// Will contain Literal and SimpleStatement nodes
298 buttons: []*Node,
299 end_token: Token,
300 };
301
302 pub const Menu = struct {
303 base: Node = .{ .id = .menu },
304 id: Token,
305 type: Token,
306 common_resource_attributes: []Token,
307 optional_statements: []*Node,
308 /// `help_id` will never be non-null if `type` is MENU
309 help_id: ?*Node,
310 begin_token: Token,
311 items: []*Node,
312 end_token: Token,
313 };
314
315 pub const MenuItem = struct {
316 base: Node = .{ .id = .menu_item },
317 menuitem: Token,
318 text: Token,
319 result: *Node,
320 option_list: []Token,
321 };
322
323 pub const MenuItemSeparator = struct {
324 base: Node = .{ .id = .menu_item_separator },
325 menuitem: Token,
326 separator: Token,
327 };
328
329 pub const MenuItemEx = struct {
330 base: Node = .{ .id = .menu_item_ex },
331 menuitem: Token,
332 text: Token,
333 id: ?*Node,
334 type: ?*Node,
335 state: ?*Node,
336 };
337
338 pub const Popup = struct {
339 base: Node = .{ .id = .popup },
340 popup: Token,
341 text: Token,
342 option_list: []Token,
343 begin_token: Token,
344 items: []*Node,
345 end_token: Token,
346 };
347
348 pub const PopupEx = struct {
349 base: Node = .{ .id = .popup_ex },
350 popup: Token,
351 text: Token,
352 id: ?*Node,
353 type: ?*Node,
354 state: ?*Node,
355 help_id: ?*Node,
356 begin_token: Token,
357 items: []*Node,
358 end_token: Token,
359 };
360
361 pub const VersionInfo = struct {
362 base: Node = .{ .id = .version_info },
363 id: Token,
364 versioninfo: Token,
365 common_resource_attributes: []Token,
366 /// Will contain VersionStatement and/or SimpleStatement nodes
367 fixed_info: []*Node,
368 begin_token: Token,
369 block_statements: []*Node,
370 end_token: Token,
371 };
372
373 /// Used for FILEVERSION and PRODUCTVERSION statements
374 pub const VersionStatement = struct {
375 base: Node = .{ .id = .version_statement },
376 type: Token,
377 /// Between 1-4 parts
378 parts: []*Node,
379 };
380
381 pub const Block = struct {
382 base: Node = .{ .id = .block },
383 /// The BLOCK token itself
384 identifier: Token,
385 key: Token,
386 /// This is undocumented but BLOCK statements support values after
387 /// the key just like VALUE statements.
388 values: []*Node,
389 begin_token: Token,
390 children: []*Node,
391 end_token: Token,
392 };
393
394 pub const BlockValue = struct {
395 base: Node = .{ .id = .block_value },
396 /// The VALUE token itself
397 identifier: Token,
398 key: Token,
399 /// These will be BlockValueValue nodes
400 values: []*Node,
401 };
402
403 pub const BlockValueValue = struct {
404 base: Node = .{ .id = .block_value_value },
405 expression: *Node,
406 /// Whether or not the value has a trailing comma is relevant
407 trailing_comma: bool,
408 };
409
410 pub const StringTable = struct {
411 base: Node = .{ .id = .string_table },
412 type: Token,
413 common_resource_attributes: []Token,
414 optional_statements: []*Node,
415 begin_token: Token,
416 strings: []*Node,
417 end_token: Token,
418 };
419
420 pub const StringTableString = struct {
421 base: Node = .{ .id = .string_table_string },
422 id: *Node,
423 maybe_comma: ?Token,
424 string: Token,
425 };
426
427 pub const LanguageStatement = struct {
428 base: Node = .{ .id = .language_statement },
429 /// The LANGUAGE token itself
430 language_token: Token,
431 primary_language_id: *Node,
432 sublanguage_id: *Node,
433 };
434
435 pub const FontStatement = struct {
436 base: Node = .{ .id = .font_statement },
437 /// The FONT token itself
438 identifier: Token,
439 point_size: *Node,
440 typeface: Token,
441 weight: ?*Node,
442 italic: ?*Node,
443 char_set: ?*Node,
444 };
445
446 /// A statement with one value associated with it.
447 /// Used for CAPTION, CHARACTERISTICS, CLASS, EXSTYLE, MENU, STYLE, VERSION,
448 /// as well as VERSIONINFO-specific statements FILEFLAGSMASK, FILEFLAGS, FILEOS,
449 /// FILETYPE, FILESUBTYPE
450 pub const SimpleStatement = struct {
451 base: Node = .{ .id = .simple_statement },
452 identifier: Token,
453 value: *Node,
454 };
455
456 pub const Invalid = struct {
457 base: Node = .{ .id = .invalid },
458 context: []Token,
459 };
460
461 pub fn isNumberExpression(node: *const Node) bool {
462 switch (node.id) {
463 .literal => {
464 const literal = @fieldParentPtr(Node.Literal, "base", node);
465 return switch (literal.token.id) {
466 .number => true,
467 else => false,
468 };
469 },
470 .binary_expression, .grouped_expression, .not_expression => return true,
471 else => return false,
472 }
473 }
474
475 pub fn isStringLiteral(node: *const Node) bool {
476 switch (node.id) {
477 .literal => {
478 const literal = @fieldParentPtr(Node.Literal, "base", node);
479 return switch (literal.token.id) {
480 .quoted_ascii_string, .quoted_wide_string => true,
481 else => false,
482 };
483 },
484 else => return false,
485 }
486 }
487
488 pub fn getFirstToken(node: *const Node) Token {
489 switch (node.id) {
490 .root => unreachable,
491 .resource_external => {
492 const casted = @fieldParentPtr(Node.ResourceExternal, "base", node);
493 return casted.id;
494 },
495 .resource_raw_data => {
496 const casted = @fieldParentPtr(Node.ResourceRawData, "base", node);
497 return casted.id;
498 },
499 .literal => {
500 const casted = @fieldParentPtr(Node.Literal, "base", node);
501 return casted.token;
502 },
503 .binary_expression => {
504 const casted = @fieldParentPtr(Node.BinaryExpression, "base", node);
505 return casted.left.getFirstToken();
506 },
507 .grouped_expression => {
508 const casted = @fieldParentPtr(Node.GroupedExpression, "base", node);
509 return casted.open_token;
510 },
511 .not_expression => {
512 const casted = @fieldParentPtr(Node.NotExpression, "base", node);
513 return casted.not_token;
514 },
515 .accelerators => {
516 const casted = @fieldParentPtr(Node.Accelerators, "base", node);
517 return casted.id;
518 },
519 .accelerator => {
520 const casted = @fieldParentPtr(Node.Accelerator, "base", node);
521 return casted.event.getFirstToken();
522 },
523 .dialog => {
524 const casted = @fieldParentPtr(Node.Dialog, "base", node);
525 return casted.id;
526 },
527 .control_statement => {
528 const casted = @fieldParentPtr(Node.ControlStatement, "base", node);
529 return casted.type;
530 },
531 .toolbar => {
532 const casted = @fieldParentPtr(Node.Toolbar, "base", node);
533 return casted.id;
534 },
535 .menu => {
536 const casted = @fieldParentPtr(Node.Menu, "base", node);
537 return casted.id;
538 },
539 inline .menu_item, .menu_item_separator, .menu_item_ex => |menu_item_type| {
540 const node_type = menu_item_type.Type();
541 const casted = @fieldParentPtr(node_type, "base", node);
542 return casted.menuitem;
543 },
544 inline .popup, .popup_ex => |popup_type| {
545 const node_type = popup_type.Type();
546 const casted = @fieldParentPtr(node_type, "base", node);
547 return casted.popup;
548 },
549 .version_info => {
550 const casted = @fieldParentPtr(Node.VersionInfo, "base", node);
551 return casted.id;
552 },
553 .version_statement => {
554 const casted = @fieldParentPtr(Node.VersionStatement, "base", node);
555 return casted.type;
556 },
557 .block => {
558 const casted = @fieldParentPtr(Node.Block, "base", node);
559 return casted.identifier;
560 },
561 .block_value => {
562 const casted = @fieldParentPtr(Node.BlockValue, "base", node);
563 return casted.identifier;
564 },
565 .block_value_value => {
566 const casted = @fieldParentPtr(Node.BlockValueValue, "base", node);
567 return casted.expression.getFirstToken();
568 },
569 .string_table => {
570 const casted = @fieldParentPtr(Node.StringTable, "base", node);
571 return casted.type;
572 },
573 .string_table_string => {
574 const casted = @fieldParentPtr(Node.StringTableString, "base", node);
575 return casted.id.getFirstToken();
576 },
577 .language_statement => {
578 const casted = @fieldParentPtr(Node.LanguageStatement, "base", node);
579 return casted.language_token;
580 },
581 .font_statement => {
582 const casted = @fieldParentPtr(Node.FontStatement, "base", node);
583 return casted.identifier;
584 },
585 .simple_statement => {
586 const casted = @fieldParentPtr(Node.SimpleStatement, "base", node);
587 return casted.identifier;
588 },
589 .invalid => {
590 const casted = @fieldParentPtr(Node.Invalid, "base", node);
591 return casted.context[0];
592 },
593 }
594 }
595
596 pub fn getLastToken(node: *const Node) Token {
597 switch (node.id) {
598 .root => unreachable,
599 .resource_external => {
600 const casted = @fieldParentPtr(Node.ResourceExternal, "base", node);
601 return casted.filename.getLastToken();
602 },
603 .resource_raw_data => {
604 const casted = @fieldParentPtr(Node.ResourceRawData, "base", node);
605 return casted.end_token;
606 },
607 .literal => {
608 const casted = @fieldParentPtr(Node.Literal, "base", node);
609 return casted.token;
610 },
611 .binary_expression => {
612 const casted = @fieldParentPtr(Node.BinaryExpression, "base", node);
613 return casted.right.getLastToken();
614 },
615 .grouped_expression => {
616 const casted = @fieldParentPtr(Node.GroupedExpression, "base", node);
617 return casted.close_token;
618 },
619 .not_expression => {
620 const casted = @fieldParentPtr(Node.NotExpression, "base", node);
621 return casted.number_token;
622 },
623 .accelerators => {
624 const casted = @fieldParentPtr(Node.Accelerators, "base", node);
625 return casted.end_token;
626 },
627 .accelerator => {
628 const casted = @fieldParentPtr(Node.Accelerator, "base", node);
629 if (casted.type_and_options.len > 0) return casted.type_and_options[casted.type_and_options.len - 1];
630 return casted.idvalue.getLastToken();
631 },
632 .dialog => {
633 const casted = @fieldParentPtr(Node.Dialog, "base", node);
634 return casted.end_token;
635 },
636 .control_statement => {
637 const casted = @fieldParentPtr(Node.ControlStatement, "base", node);
638 if (casted.extra_data_end) |token| return token;
639 if (casted.help_id) |help_id_node| return help_id_node.getLastToken();
640 if (casted.exstyle) |exstyle_node| return exstyle_node.getLastToken();
641 // For user-defined CONTROL controls, the style comes before 'x', but
642 // otherwise it comes after 'height' so it could be the last token if
643 // it's present.
644 if (!casted.isUserDefined()) {
645 if (casted.style) |style_node| return style_node.getLastToken();
646 }
647 return casted.height.getLastToken();
648 },
649 .toolbar => {
650 const casted = @fieldParentPtr(Node.Toolbar, "base", node);
651 return casted.end_token;
652 },
653 .menu => {
654 const casted = @fieldParentPtr(Node.Menu, "base", node);
655 return casted.end_token;
656 },
657 .menu_item => {
658 const casted = @fieldParentPtr(Node.MenuItem, "base", node);
659 if (casted.option_list.len > 0) return casted.option_list[casted.option_list.len - 1];
660 return casted.result.getLastToken();
661 },
662 .menu_item_separator => {
663 const casted = @fieldParentPtr(Node.MenuItemSeparator, "base", node);
664 return casted.separator;
665 },
666 .menu_item_ex => {
667 const casted = @fieldParentPtr(Node.MenuItemEx, "base", node);
668 if (casted.state) |state_node| return state_node.getLastToken();
669 if (casted.type) |type_node| return type_node.getLastToken();
670 if (casted.id) |id_node| return id_node.getLastToken();
671 return casted.text;
672 },
673 inline .popup, .popup_ex => |popup_type| {
674 const node_type = popup_type.Type();
675 const casted = @fieldParentPtr(node_type, "base", node);
676 return casted.end_token;
677 },
678 .version_info => {
679 const casted = @fieldParentPtr(Node.VersionInfo, "base", node);
680 return casted.end_token;
681 },
682 .version_statement => {
683 const casted = @fieldParentPtr(Node.VersionStatement, "base", node);
684 return casted.parts[casted.parts.len - 1].getLastToken();
685 },
686 .block => {
687 const casted = @fieldParentPtr(Node.Block, "base", node);
688 return casted.end_token;
689 },
690 .block_value => {
691 const casted = @fieldParentPtr(Node.BlockValue, "base", node);
692 if (casted.values.len > 0) return casted.values[casted.values.len - 1].getLastToken();
693 return casted.key;
694 },
695 .block_value_value => {
696 const casted = @fieldParentPtr(Node.BlockValueValue, "base", node);
697 return casted.expression.getLastToken();
698 },
699 .string_table => {
700 const casted = @fieldParentPtr(Node.StringTable, "base", node);
701 return casted.end_token;
702 },
703 .string_table_string => {
704 const casted = @fieldParentPtr(Node.StringTableString, "base", node);
705 return casted.string;
706 },
707 .language_statement => {
708 const casted = @fieldParentPtr(Node.LanguageStatement, "base", node);
709 return casted.sublanguage_id.getLastToken();
710 },
711 .font_statement => {
712 const casted = @fieldParentPtr(Node.FontStatement, "base", node);
713 if (casted.char_set) |char_set_node| return char_set_node.getLastToken();
714 if (casted.italic) |italic_node| return italic_node.getLastToken();
715 if (casted.weight) |weight_node| return weight_node.getLastToken();
716 return casted.typeface;
717 },
718 .simple_statement => {
719 const casted = @fieldParentPtr(Node.SimpleStatement, "base", node);
720 return casted.value.getLastToken();
721 },
722 .invalid => {
723 const casted = @fieldParentPtr(Node.Invalid, "base", node);
724 return casted.context[casted.context.len - 1];
725 },
726 }
727 }
728
729 pub fn dump(
730 node: *const Node,
731 tree: *const Tree,
732 writer: anytype,
733 indent: usize,
734 ) @TypeOf(writer).Error!void {
735 try writer.writeByteNTimes(' ', indent);
736 try writer.writeAll(@tagName(node.id));
737 switch (node.id) {
738 .root => {
739 try writer.writeAll("\n");
740 const root = @fieldParentPtr(Node.Root, "base", node);
741 for (root.body) |body_node| {
742 try body_node.dump(tree, writer, indent + 1);
743 }
744 },
745 .resource_external => {
746 const resource = @fieldParentPtr(Node.ResourceExternal, "base", node);
747 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len });
748 try resource.filename.dump(tree, writer, indent + 1);
749 },
750 .resource_raw_data => {
751 const resource = @fieldParentPtr(Node.ResourceRawData, "base", node);
752 try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len });
753 for (resource.raw_data) |data_expression| {
754 try data_expression.dump(tree, writer, indent + 1);
755 }
756 },
757 .literal => {
758 const literal = @fieldParentPtr(Node.Literal, "base", node);
759 try writer.writeAll(" ");
760 try writer.writeAll(literal.token.slice(tree.source));
761 try writer.writeAll("\n");
762 },
763 .binary_expression => {
764 const binary = @fieldParentPtr(Node.BinaryExpression, "base", node);
765 try writer.writeAll(" ");
766 try writer.writeAll(binary.operator.slice(tree.source));
767 try writer.writeAll("\n");
768 try binary.left.dump(tree, writer, indent + 1);
769 try binary.right.dump(tree, writer, indent + 1);
770 },
771 .grouped_expression => {
772 const grouped = @fieldParentPtr(Node.GroupedExpression, "base", node);
773 try writer.writeAll("\n");
774 try writer.writeByteNTimes(' ', indent);
775 try writer.writeAll(grouped.open_token.slice(tree.source));
776 try writer.writeAll("\n");
777 try grouped.expression.dump(tree, writer, indent + 1);
778 try writer.writeByteNTimes(' ', indent);
779 try writer.writeAll(grouped.close_token.slice(tree.source));
780 try writer.writeAll("\n");
781 },
782 .not_expression => {
783 const not = @fieldParentPtr(Node.NotExpression, "base", node);
784 try writer.writeAll(" ");
785 try writer.writeAll(not.not_token.slice(tree.source));
786 try writer.writeAll(" ");
787 try writer.writeAll(not.number_token.slice(tree.source));
788 try writer.writeAll("\n");
789 },
790 .accelerators => {
791 const accelerators = @fieldParentPtr(Node.Accelerators, "base", node);
792 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len });
793 for (accelerators.optional_statements) |statement| {
794 try statement.dump(tree, writer, indent + 1);
795 }
796 try writer.writeByteNTimes(' ', indent);
797 try writer.writeAll(accelerators.begin_token.slice(tree.source));
798 try writer.writeAll("\n");
799 for (accelerators.accelerators) |accelerator| {
800 try accelerator.dump(tree, writer, indent + 1);
801 }
802 try writer.writeByteNTimes(' ', indent);
803 try writer.writeAll(accelerators.end_token.slice(tree.source));
804 try writer.writeAll("\n");
805 },
806 .accelerator => {
807 const accelerator = @fieldParentPtr(Node.Accelerator, "base", node);
808 for (accelerator.type_and_options, 0..) |option, i| {
809 if (i != 0) try writer.writeAll(",");
810 try writer.writeByte(' ');
811 try writer.writeAll(option.slice(tree.source));
812 }
813 try writer.writeAll("\n");
814 try accelerator.event.dump(tree, writer, indent + 1);
815 try accelerator.idvalue.dump(tree, writer, indent + 1);
816 },
817 .dialog => {
818 const dialog = @fieldParentPtr(Node.Dialog, "base", node);
819 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len });
820 inline for (.{ "x", "y", "width", "height" }) |arg| {
821 try writer.writeByteNTimes(' ', indent + 1);
822 try writer.writeAll(arg ++ ":\n");
823 try @field(dialog, arg).dump(tree, writer, indent + 2);
824 }
825 if (dialog.help_id) |help_id| {
826 try writer.writeByteNTimes(' ', indent + 1);
827 try writer.writeAll("help_id:\n");
828 try help_id.dump(tree, writer, indent + 2);
829 }
830 for (dialog.optional_statements) |statement| {
831 try statement.dump(tree, writer, indent + 1);
832 }
833 try writer.writeByteNTimes(' ', indent);
834 try writer.writeAll(dialog.begin_token.slice(tree.source));
835 try writer.writeAll("\n");
836 for (dialog.controls) |control| {
837 try control.dump(tree, writer, indent + 1);
838 }
839 try writer.writeByteNTimes(' ', indent);
840 try writer.writeAll(dialog.end_token.slice(tree.source));
841 try writer.writeAll("\n");
842 },
843 .control_statement => {
844 const control = @fieldParentPtr(Node.ControlStatement, "base", node);
845 try writer.print(" {s}", .{control.type.slice(tree.source)});
846 if (control.text) |text| {
847 try writer.print(" text: {s}", .{text.slice(tree.source)});
848 }
849 try writer.writeByte('\n');
850 if (control.class) |class| {
851 try writer.writeByteNTimes(' ', indent + 1);
852 try writer.writeAll("class:\n");
853 try class.dump(tree, writer, indent + 2);
854 }
855 inline for (.{ "id", "x", "y", "width", "height" }) |arg| {
856 try writer.writeByteNTimes(' ', indent + 1);
857 try writer.writeAll(arg ++ ":\n");
858 try @field(control, arg).dump(tree, writer, indent + 2);
859 }
860 inline for (.{ "style", "exstyle", "help_id" }) |arg| {
861 if (@field(control, arg)) |val_node| {
862 try writer.writeByteNTimes(' ', indent + 1);
863 try writer.writeAll(arg ++ ":\n");
864 try val_node.dump(tree, writer, indent + 2);
865 }
866 }
867 if (control.extra_data_begin != null) {
868 try writer.writeByteNTimes(' ', indent);
869 try writer.writeAll(control.extra_data_begin.?.slice(tree.source));
870 try writer.writeAll("\n");
871 for (control.extra_data) |data_node| {
872 try data_node.dump(tree, writer, indent + 1);
873 }
874 try writer.writeByteNTimes(' ', indent);
875 try writer.writeAll(control.extra_data_end.?.slice(tree.source));
876 try writer.writeAll("\n");
877 }
878 },
879 .toolbar => {
880 const toolbar = @fieldParentPtr(Node.Toolbar, "base", node);
881 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len });
882 inline for (.{ "button_width", "button_height" }) |arg| {
883 try writer.writeByteNTimes(' ', indent + 1);
884 try writer.writeAll(arg ++ ":\n");
885 try @field(toolbar, arg).dump(tree, writer, indent + 2);
886 }
887 try writer.writeByteNTimes(' ', indent);
888 try writer.writeAll(toolbar.begin_token.slice(tree.source));
889 try writer.writeAll("\n");
890 for (toolbar.buttons) |button_or_sep| {
891 try button_or_sep.dump(tree, writer, indent + 1);
892 }
893 try writer.writeByteNTimes(' ', indent);
894 try writer.writeAll(toolbar.end_token.slice(tree.source));
895 try writer.writeAll("\n");
896 },
897 .menu => {
898 const menu = @fieldParentPtr(Node.Menu, "base", node);
899 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len });
900 for (menu.optional_statements) |statement| {
901 try statement.dump(tree, writer, indent + 1);
902 }
903 if (menu.help_id) |help_id| {
904 try writer.writeByteNTimes(' ', indent + 1);
905 try writer.writeAll("help_id:\n");
906 try help_id.dump(tree, writer, indent + 2);
907 }
908 try writer.writeByteNTimes(' ', indent);
909 try writer.writeAll(menu.begin_token.slice(tree.source));
910 try writer.writeAll("\n");
911 for (menu.items) |item| {
912 try item.dump(tree, writer, indent + 1);
913 }
914 try writer.writeByteNTimes(' ', indent);
915 try writer.writeAll(menu.end_token.slice(tree.source));
916 try writer.writeAll("\n");
917 },
918 .menu_item => {
919 const menu_item = @fieldParentPtr(Node.MenuItem, "base", node);
920 try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len });
921 try menu_item.result.dump(tree, writer, indent + 1);
922 },
923 .menu_item_separator => {
924 const menu_item = @fieldParentPtr(Node.MenuItemSeparator, "base", node);
925 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) });
926 },
927 .menu_item_ex => {
928 const menu_item = @fieldParentPtr(Node.MenuItemEx, "base", node);
929 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) });
930 inline for (.{ "id", "type", "state" }) |arg| {
931 if (@field(menu_item, arg)) |val_node| {
932 try writer.writeByteNTimes(' ', indent + 1);
933 try writer.writeAll(arg ++ ":\n");
934 try val_node.dump(tree, writer, indent + 2);
935 }
936 }
937 },
938 .popup => {
939 const popup = @fieldParentPtr(Node.Popup, "base", node);
940 try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len });
941 try writer.writeByteNTimes(' ', indent);
942 try writer.writeAll(popup.begin_token.slice(tree.source));
943 try writer.writeAll("\n");
944 for (popup.items) |item| {
945 try item.dump(tree, writer, indent + 1);
946 }
947 try writer.writeByteNTimes(' ', indent);
948 try writer.writeAll(popup.end_token.slice(tree.source));
949 try writer.writeAll("\n");
950 },
951 .popup_ex => {
952 const popup = @fieldParentPtr(Node.PopupEx, "base", node);
953 try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) });
954 inline for (.{ "id", "type", "state", "help_id" }) |arg| {
955 if (@field(popup, arg)) |val_node| {
956 try writer.writeByteNTimes(' ', indent + 1);
957 try writer.writeAll(arg ++ ":\n");
958 try val_node.dump(tree, writer, indent + 2);
959 }
960 }
961 try writer.writeByteNTimes(' ', indent);
962 try writer.writeAll(popup.begin_token.slice(tree.source));
963 try writer.writeAll("\n");
964 for (popup.items) |item| {
965 try item.dump(tree, writer, indent + 1);
966 }
967 try writer.writeByteNTimes(' ', indent);
968 try writer.writeAll(popup.end_token.slice(tree.source));
969 try writer.writeAll("\n");
970 },
971 .version_info => {
972 const version_info = @fieldParentPtr(Node.VersionInfo, "base", node);
973 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len });
974 for (version_info.fixed_info) |fixed_info| {
975 try fixed_info.dump(tree, writer, indent + 1);
976 }
977 try writer.writeByteNTimes(' ', indent);
978 try writer.writeAll(version_info.begin_token.slice(tree.source));
979 try writer.writeAll("\n");
980 for (version_info.block_statements) |block| {
981 try block.dump(tree, writer, indent + 1);
982 }
983 try writer.writeByteNTimes(' ', indent);
984 try writer.writeAll(version_info.end_token.slice(tree.source));
985 try writer.writeAll("\n");
986 },
987 .version_statement => {
988 const version_statement = @fieldParentPtr(Node.VersionStatement, "base", node);
989 try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)});
990 for (version_statement.parts) |part| {
991 try part.dump(tree, writer, indent + 1);
992 }
993 },
994 .block => {
995 const block = @fieldParentPtr(Node.Block, "base", node);
996 try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) });
997 for (block.values) |value| {
998 try value.dump(tree, writer, indent + 1);
999 }
1000 try writer.writeByteNTimes(' ', indent);
1001 try writer.writeAll(block.begin_token.slice(tree.source));
1002 try writer.writeAll("\n");
1003 for (block.children) |child| {
1004 try child.dump(tree, writer, indent + 1);
1005 }
1006 try writer.writeByteNTimes(' ', indent);
1007 try writer.writeAll(block.end_token.slice(tree.source));
1008 try writer.writeAll("\n");
1009 },
1010 .block_value => {
1011 const block_value = @fieldParentPtr(Node.BlockValue, "base", node);
1012 try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) });
1013 for (block_value.values) |value| {
1014 try value.dump(tree, writer, indent + 1);
1015 }
1016 },
1017 .block_value_value => {
1018 const block_value = @fieldParentPtr(Node.BlockValueValue, "base", node);
1019 if (block_value.trailing_comma) {
1020 try writer.writeAll(" ,");
1021 }
1022 try writer.writeAll("\n");
1023 try block_value.expression.dump(tree, writer, indent + 1);
1024 },
1025 .string_table => {
1026 const string_table = @fieldParentPtr(Node.StringTable, "base", node);
1027 try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len });
1028 for (string_table.optional_statements) |statement| {
1029 try statement.dump(tree, writer, indent + 1);
1030 }
1031 try writer.writeByteNTimes(' ', indent);
1032 try writer.writeAll(string_table.begin_token.slice(tree.source));
1033 try writer.writeAll("\n");
1034 for (string_table.strings) |string| {
1035 try string.dump(tree, writer, indent + 1);
1036 }
1037 try writer.writeByteNTimes(' ', indent);
1038 try writer.writeAll(string_table.end_token.slice(tree.source));
1039 try writer.writeAll("\n");
1040 },
1041 .string_table_string => {
1042 try writer.writeAll("\n");
1043 const string = @fieldParentPtr(Node.StringTableString, "base", node);
1044 try string.id.dump(tree, writer, indent + 1);
1045 try writer.writeByteNTimes(' ', indent + 1);
1046 try writer.print("{s}\n", .{string.string.slice(tree.source)});
1047 },
1048 .language_statement => {
1049 const language = @fieldParentPtr(Node.LanguageStatement, "base", node);
1050 try writer.print(" {s}\n", .{language.language_token.slice(tree.source)});
1051 try language.primary_language_id.dump(tree, writer, indent + 1);
1052 try language.sublanguage_id.dump(tree, writer, indent + 1);
1053 },
1054 .font_statement => {
1055 const font = @fieldParentPtr(Node.FontStatement, "base", node);
1056 try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) });
1057 try writer.writeByteNTimes(' ', indent + 1);
1058 try writer.writeAll("point_size:\n");
1059 try font.point_size.dump(tree, writer, indent + 2);
1060 inline for (.{ "weight", "italic", "char_set" }) |arg| {
1061 if (@field(font, arg)) |arg_node| {
1062 try writer.writeByteNTimes(' ', indent + 1);
1063 try writer.writeAll(arg ++ ":\n");
1064 try arg_node.dump(tree, writer, indent + 2);
1065 }
1066 }
1067 },
1068 .simple_statement => {
1069 const statement = @fieldParentPtr(Node.SimpleStatement, "base", node);
1070 try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)});
1071 try statement.value.dump(tree, writer, indent + 1);
1072 },
1073 .invalid => {
1074 const invalid = @fieldParentPtr(Node.Invalid, "base", node);
1075 try writer.print(" context.len: {}\n", .{invalid.context.len});
1076 for (invalid.context) |context_token| {
1077 try writer.writeByteNTimes(' ', indent + 1);
1078 try writer.print("{s}:{s}", .{ @tagName(context_token.id), context_token.slice(tree.source) });
1079 try writer.writeByte('\n');
1080 }
1081 },
1082 }
1083 }
1084};
lib/compiler/resinator/bmp.zig created+270
...@@ -0,0 +1,270 @@
1//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
2//! https://learn.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)
3//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
4//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
5//! https://archive.org/details/mac_Graphics_File_Formats_Second_Edition_1996/page/n607/mode/2up
6//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
7//!
8//! Notes:
9//! - The Microsoft documentation is incredibly unclear about the color table when the
10//! bit depth is >= 16.
11//! + For bit depth 24 it says "the bmiColors member of BITMAPINFO is NULL" but also
12//! says "the bmiColors color table is used for optimizing colors used on palette-based
13//! devices, and must contain the number of entries specified by the bV5ClrUsed member"
14//! + For bit depth 16 and 32, it seems to imply that if the compression is BI_BITFIELDS
15//! or BI_ALPHABITFIELDS, then the color table *only* consists of the bit masks, but
16//! doesn't really say this outright and the Wikipedia article seems to disagree
17//! For the purposes of this implementation, color tables can always be present for any
18//! bit depth and compression, and the color table follows the header + any optional
19//! bit mask fields dictated by the specified compression.
20
21const std = @import("std");
22const BitmapHeader = @import("ico.zig").BitmapHeader;
23const builtin = @import("builtin");
24const native_endian = builtin.cpu.arch.endian();
25
26pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian);
27pub const file_header_len = 14;
28
29pub const ReadError = error{
30 UnexpectedEOF,
31 InvalidFileHeader,
32 ImpossiblePixelDataOffset,
33 UnknownBitmapVersion,
34 InvalidBitsPerPixel,
35 TooManyColorsInPalette,
36 MissingBitfieldMasks,
37};
38
39pub const BitmapInfo = struct {
40 dib_header_size: u32,
41 /// Contains the interpreted number of colors in the palette (e.g.
42 /// if the field's value is zero and the bit depth is <= 8, this
43 /// will contain the maximum number of colors for the bit depth
44 /// rather than the field's value directly).
45 colors_in_palette: u32,
46 bytes_per_color_palette_element: u8,
47 pixel_data_offset: u32,
48 compression: Compression,
49
50 pub fn getExpectedPaletteByteLen(self: *const BitmapInfo) u64 {
51 return @as(u64, self.colors_in_palette) * self.bytes_per_color_palette_element;
52 }
53
54 pub fn getActualPaletteByteLen(self: *const BitmapInfo) u64 {
55 return self.getByteLenBetweenHeadersAndPixels() - self.getBitmasksByteLen();
56 }
57
58 pub fn getByteLenBetweenHeadersAndPixels(self: *const BitmapInfo) u64 {
59 return @as(u64, self.pixel_data_offset) - self.dib_header_size - file_header_len;
60 }
61
62 pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 {
63 return switch (self.compression) {
64 .BI_BITFIELDS => 12,
65 .BI_ALPHABITFIELDS => 16,
66 else => 0,
67 };
68 }
69
70 pub fn getMissingPaletteByteLen(self: *const BitmapInfo) u64 {
71 if (self.getActualPaletteByteLen() >= self.getExpectedPaletteByteLen()) return 0;
72 return self.getExpectedPaletteByteLen() - self.getActualPaletteByteLen();
73 }
74
75 /// Returns the full byte len of the DIB header + optional bitmasks + color palette
76 pub fn getExpectedByteLenBeforePixelData(self: *const BitmapInfo) u64 {
77 return @as(u64, self.dib_header_size) + self.getBitmasksByteLen() + self.getExpectedPaletteByteLen();
78 }
79
80 /// Returns the full expected byte len
81 pub fn getExpectedByteLen(self: *const BitmapInfo, file_size: u64) u64 {
82 return self.getExpectedByteLenBeforePixelData() + self.getPixelDataLen(file_size);
83 }
84
85 pub fn getPixelDataLen(self: *const BitmapInfo, file_size: u64) u64 {
86 return file_size - self.pixel_data_offset;
87 }
88};
89
90pub fn read(reader: anytype, max_size: u64) ReadError!BitmapInfo {
91 var bitmap_info: BitmapInfo = undefined;
92 const file_header = reader.readBytesNoEof(file_header_len) catch return error.UnexpectedEOF;
93
94 const id = std.mem.readInt(u16, file_header[0..2], native_endian);
95 if (id != windows_format_id) return error.InvalidFileHeader;
96
97 bitmap_info.pixel_data_offset = std.mem.readInt(u32, file_header[10..14], .little);
98 if (bitmap_info.pixel_data_offset > max_size) return error.ImpossiblePixelDataOffset;
99
100 bitmap_info.dib_header_size = reader.readInt(u32, .little) catch return error.UnexpectedEOF;
101 if (bitmap_info.pixel_data_offset < file_header_len + bitmap_info.dib_header_size) return error.ImpossiblePixelDataOffset;
102 const dib_version = BitmapHeader.Version.get(bitmap_info.dib_header_size);
103 switch (dib_version) {
104 .@"nt3.1", .@"nt4.0", .@"nt5.0" => {
105 var dib_header_buf: [@sizeOf(BITMAPINFOHEADER)]u8 align(@alignOf(BITMAPINFOHEADER)) = undefined;
106 std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
107 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF;
108 var dib_header: *BITMAPINFOHEADER = @ptrCast(&dib_header_buf);
109 structFieldsLittleToNative(BITMAPINFOHEADER, dib_header);
110
111 bitmap_info.colors_in_palette = try dib_header.numColorsInTable();
112 bitmap_info.bytes_per_color_palette_element = 4;
113 bitmap_info.compression = @enumFromInt(dib_header.biCompression);
114
115 if (bitmap_info.getByteLenBetweenHeadersAndPixels() < bitmap_info.getBitmasksByteLen()) {
116 return error.MissingBitfieldMasks;
117 }
118 },
119 .@"win2.0" => {
120 var dib_header_buf: [@sizeOf(BITMAPCOREHEADER)]u8 align(@alignOf(BITMAPCOREHEADER)) = undefined;
121 std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
122 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF;
123 const dib_header: *BITMAPCOREHEADER = @ptrCast(&dib_header_buf);
124 structFieldsLittleToNative(BITMAPCOREHEADER, dib_header);
125
126 // > The size of the color palette is calculated from the BitsPerPixel value.
127 // > The color palette has 2, 16, 256, or 0 entries for a BitsPerPixel of
128 // > 1, 4, 8, and 24, respectively.
129 bitmap_info.colors_in_palette = switch (dib_header.bcBitCount) {
130 inline 1, 4, 8 => |bit_count| 1 << bit_count,
131 24 => 0,
132 else => return error.InvalidBitsPerPixel,
133 };
134 bitmap_info.bytes_per_color_palette_element = 3;
135
136 bitmap_info.compression = .BI_RGB;
137 },
138 .unknown => return error.UnknownBitmapVersion,
139 }
140
141 return bitmap_info;
142}
143
144/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
145pub const BITMAPCOREHEADER = extern struct {
146 bcSize: u32,
147 bcWidth: u16,
148 bcHeight: u16,
149 bcPlanes: u16,
150 bcBitCount: u16,
151};
152
153/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
154pub const BITMAPINFOHEADER = extern struct {
155 bcSize: u32,
156 biWidth: i32,
157 biHeight: i32,
158 biPlanes: u16,
159 biBitCount: u16,
160 biCompression: u32,
161 biSizeImage: u32,
162 biXPelsPerMeter: i32,
163 biYPelsPerMeter: i32,
164 biClrUsed: u32,
165 biClrImportant: u32,
166
167 /// Returns error.TooManyColorsInPalette if the number of colors specified
168 /// exceeds the number of possible colors referenced in the pixel data (i.e.
169 /// if 1 bit is used per pixel, then the color table can't have more than 2 colors
170 /// since any more couldn't possibly be indexed in the pixel data)
171 ///
172 /// Returns error.InvalidBitsPerPixel if the bit depth is not 1, 4, 8, 16, 24, or 32.
173 pub fn numColorsInTable(self: BITMAPINFOHEADER) !u32 {
174 switch (self.biBitCount) {
175 inline 1, 4, 8 => |bit_count| switch (self.biClrUsed) {
176 // > If biClrUsed is zero, the array contains the maximum number of
177 // > colors for the given bitdepth; that is, 2^biBitCount colors
178 0 => return 1 << bit_count,
179 // > If biClrUsed is nonzero and the biBitCount member is less than 16,
180 // > the biClrUsed member specifies the actual number of colors the
181 // > graphics engine or device driver accesses.
182 else => {
183 const max_colors = 1 << bit_count;
184 if (self.biClrUsed > max_colors) {
185 return error.TooManyColorsInPalette;
186 }
187 return self.biClrUsed;
188 },
189 },
190 // > If biBitCount is 16 or greater, the biClrUsed member specifies
191 // > the size of the color table used to optimize performance of the
192 // > system color palettes.
193 //
194 // Note: Bit depths >= 16 only use the color table 'for optimizing colors
195 // used on palette-based devices', but it still makes sense to limit their
196 // colors since the pixel data is still limited to this number of colors
197 // (i.e. even though the color table is not indexed by the pixel data,
198 // the color table having more colors than the pixel data can represent
199 // would never make sense and indicates a malformed bitmap).
200 inline 16, 24, 32 => |bit_count| {
201 const max_colors = 1 << bit_count;
202 if (self.biClrUsed > max_colors) {
203 return error.TooManyColorsInPalette;
204 }
205 return self.biClrUsed;
206 },
207 else => return error.InvalidBitsPerPixel,
208 }
209 }
210};
211
212pub const Compression = enum(u32) {
213 BI_RGB = 0,
214 BI_RLE8 = 1,
215 BI_RLE4 = 2,
216 BI_BITFIELDS = 3,
217 BI_JPEG = 4,
218 BI_PNG = 5,
219 BI_ALPHABITFIELDS = 6,
220 BI_CMYK = 11,
221 BI_CMYKRLE8 = 12,
222 BI_CMYKRLE4 = 13,
223 _,
224};
225
226fn structFieldsLittleToNative(comptime T: type, x: *T) void {
227 inline for (@typeInfo(T).Struct.fields) |field| {
228 @field(x, field.name) = std.mem.littleToNative(field.type, @field(x, field.name));
229 }
230}
231
232test "read" {
233 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".*;
234 var fbs = std.io.fixedBufferStream(&bmp_data);
235
236 {
237 const bitmap = try read(fbs.reader(), bmp_data.len);
238 try std.testing.expectEqual(@as(u32, BitmapHeader.Version.@"nt3.1".len()), bitmap.dib_header_size);
239 }
240
241 {
242 fbs.reset();
243 bmp_data[file_header_len] = 11;
244 try std.testing.expectError(error.UnknownBitmapVersion, read(fbs.reader(), bmp_data.len));
245
246 // restore
247 bmp_data[file_header_len] = BitmapHeader.Version.@"nt3.1".len();
248 }
249
250 {
251 fbs.reset();
252 bmp_data[0] = 'b';
253 try std.testing.expectError(error.InvalidFileHeader, read(fbs.reader(), bmp_data.len));
254
255 // restore
256 bmp_data[0] = 'B';
257 }
258
259 {
260 const cutoff_len = file_header_len + BitmapHeader.Version.@"nt3.1".len() - 1;
261 var dib_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]);
262 try std.testing.expectError(error.UnexpectedEOF, read(dib_cutoff_fbs.reader(), bmp_data.len));
263 }
264
265 {
266 const cutoff_len = file_header_len - 1;
267 var bmp_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]);
268 try std.testing.expectError(error.UnexpectedEOF, read(bmp_cutoff_fbs.reader(), bmp_data.len));
269 }
270}
lib/compiler/resinator/cli.zig created+1507
...@@ -0,0 +1,1507 @@
1const std = @import("std");
2const CodePage = @import("code_pages.zig").CodePage;
3const lang = @import("lang.zig");
4const res = @import("res.zig");
5const Allocator = std.mem.Allocator;
6const lex = @import("lex.zig");
7
8/// This is what /SL 100 will set the maximum string literal length to
9pub const max_string_literal_length_100_percent = 8192;
10
11pub const usage_string_after_command_name =
12 \\ [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
54 \\ none Do not use any autodetected include paths
55 \\ /:depfile <path> Output a file containing a list of all the files that
56 \\ the .rc includes or otherwise depends on.
57 \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set.
58 \\ json (default) A top-level JSON array of paths
59 \\ /:mingw-includes <path> Path to a directory containing MinGW include files. If
60 \\ not specified, bundled MinGW include files will be used.
61 \\
62 \\Note: For compatibility reasons, all custom options start with :
63 \\
64;
65
66pub fn writeUsage(writer: anytype, command_name: []const u8) !void {
67 try writer.writeAll("Usage: ");
68 try writer.writeAll(command_name);
69 try writer.writeAll(usage_string_after_command_name);
70}
71
72pub const Diagnostics = struct {
73 errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
74 allocator: Allocator,
75
76 pub const ErrorDetails = struct {
77 arg_index: usize,
78 arg_span: ArgSpan = .{},
79 msg: std.ArrayListUnmanaged(u8) = .{},
80 type: Type = .err,
81 print_args: bool = true,
82
83 pub const Type = enum { err, warning, note };
84 pub const ArgSpan = struct {
85 point_at_next_arg: bool = false,
86 name_offset: usize = 0,
87 prefix_len: usize = 0,
88 value_offset: usize = 0,
89 name_len: usize = 0,
90 };
91 };
92
93 pub fn init(allocator: Allocator) Diagnostics {
94 return .{
95 .allocator = allocator,
96 };
97 }
98
99 pub fn deinit(self: *Diagnostics) void {
100 for (self.errors.items) |*details| {
101 details.msg.deinit(self.allocator);
102 }
103 self.errors.deinit(self.allocator);
104 }
105
106 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
107 try self.errors.append(self.allocator, error_details);
108 }
109
110 pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8, config: std.io.tty.Config) void {
111 std.debug.getStderrMutex().lock();
112 defer std.debug.getStderrMutex().unlock();
113 const stderr = std.io.getStdErr().writer();
114 self.renderToWriter(args, stderr, config) catch return;
115 }
116
117 pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: anytype, config: std.io.tty.Config) !void {
118 for (self.errors.items) |err_details| {
119 try renderErrorMessage(writer, config, err_details, args);
120 }
121 }
122
123 pub fn hasError(self: *const Diagnostics) bool {
124 for (self.errors.items) |err| {
125 if (err.type == .err) return true;
126 }
127 return false;
128 }
129};
130
131pub const Options = struct {
132 allocator: Allocator,
133 input_filename: []const u8 = &[_]u8{},
134 output_filename: []const u8 = &[_]u8{},
135 extra_include_paths: std.ArrayListUnmanaged([]const u8) = .{},
136 ignore_include_env_var: bool = false,
137 preprocess: Preprocess = .yes,
138 default_language_id: ?u16 = null,
139 default_code_page: ?CodePage = null,
140 verbose: bool = false,
141 symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .{},
142 null_terminate_string_table_strings: bool = false,
143 max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
144 silent_duplicate_control_ids: bool = false,
145 warn_instead_of_error_on_invalid_code_page: bool = false,
146 debug: bool = false,
147 print_help_and_exit: bool = false,
148 auto_includes: AutoIncludes = .any,
149 depfile_path: ?[]const u8 = null,
150 depfile_fmt: DepfileFormat = .json,
151 mingw_includes_dir: ?[]const u8 = null,
152
153 pub const AutoIncludes = enum { any, msvc, gnu, none };
154 pub const DepfileFormat = enum { json };
155 pub const Preprocess = enum { no, yes, only };
156 pub const SymbolAction = enum { define, undefine };
157 pub const SymbolValue = union(SymbolAction) {
158 define: []const u8,
159 undefine: void,
160
161 pub fn deinit(self: SymbolValue, allocator: Allocator) void {
162 switch (self) {
163 .define => |value| allocator.free(value),
164 .undefine => {},
165 }
166 }
167 };
168
169 /// Does not check that identifier contains only valid characters
170 pub fn define(self: *Options, identifier: []const u8, value: []const u8) !void {
171 if (self.symbols.getPtr(identifier)) |val_ptr| {
172 // If the symbol is undefined, then that always takes precedence so
173 // we shouldn't change anything.
174 if (val_ptr.* == .undefine) return;
175 // Otherwise, the new value takes precedence.
176 const duped_value = try self.allocator.dupe(u8, value);
177 errdefer self.allocator.free(duped_value);
178 val_ptr.deinit(self.allocator);
179 val_ptr.* = .{ .define = duped_value };
180 return;
181 }
182 const duped_key = try self.allocator.dupe(u8, identifier);
183 errdefer self.allocator.free(duped_key);
184 const duped_value = try self.allocator.dupe(u8, value);
185 errdefer self.allocator.free(duped_value);
186 try self.symbols.put(self.allocator, duped_key, .{ .define = duped_value });
187 }
188
189 /// Does not check that identifier contains only valid characters
190 pub fn undefine(self: *Options, identifier: []const u8) !void {
191 if (self.symbols.getPtr(identifier)) |action| {
192 action.deinit(self.allocator);
193 action.* = .{ .undefine = {} };
194 return;
195 }
196 const duped_key = try self.allocator.dupe(u8, identifier);
197 errdefer self.allocator.free(duped_key);
198 try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} });
199 }
200
201 /// If the current input filename both:
202 /// - does not have an extension, and
203 /// - does not exist in the cwd
204 /// then this function will append `.rc` to the input filename
205 ///
206 /// Note: This behavior is different from the Win32 compiler.
207 /// It always appends .RC if the filename does not have
208 /// a `.` in it and it does not even try the verbatim name
209 /// in that scenario.
210 ///
211 /// The approach taken here is meant to give us a 'best of both
212 /// worlds' situation where we'll be compatible with most use-cases
213 /// of the .rc extension being omitted from the CLI args, but still
214 /// work fine if the file itself does not have an extension.
215 pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void {
216 if (std.fs.path.extension(options.input_filename).len == 0) {
217 cwd.access(options.input_filename, .{}) catch |err| switch (err) {
218 error.FileNotFound => {
219 var filename_bytes = try options.allocator.alloc(u8, options.input_filename.len + 3);
220 @memcpy(filename_bytes[0..options.input_filename.len], options.input_filename);
221 @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc");
222 options.allocator.free(options.input_filename);
223 options.input_filename = filename_bytes;
224 },
225 else => {},
226 };
227 }
228 }
229
230 pub fn deinit(self: *Options) void {
231 for (self.extra_include_paths.items) |extra_include_path| {
232 self.allocator.free(extra_include_path);
233 }
234 self.extra_include_paths.deinit(self.allocator);
235 self.allocator.free(self.input_filename);
236 self.allocator.free(self.output_filename);
237 var symbol_it = self.symbols.iterator();
238 while (symbol_it.next()) |entry| {
239 self.allocator.free(entry.key_ptr.*);
240 entry.value_ptr.deinit(self.allocator);
241 }
242 self.symbols.deinit(self.allocator);
243 if (self.depfile_path) |depfile_path| {
244 self.allocator.free(depfile_path);
245 }
246 if (self.mingw_includes_dir) |mingw_includes_dir| {
247 self.allocator.free(mingw_includes_dir);
248 }
249 }
250
251 pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
252 try writer.print("Input filename: {s}\n", .{self.input_filename});
253 try writer.print("Output filename: {s}\n", .{self.output_filename});
254 if (self.extra_include_paths.items.len > 0) {
255 try writer.writeAll(" Extra include paths:\n");
256 for (self.extra_include_paths.items) |extra_include_path| {
257 try writer.print(" \"{s}\"\n", .{extra_include_path});
258 }
259 }
260 if (self.ignore_include_env_var) {
261 try writer.writeAll(" The INCLUDE environment variable will be ignored\n");
262 }
263 if (self.preprocess == .no) {
264 try writer.writeAll(" The preprocessor will not be invoked\n");
265 } else if (self.preprocess == .only) {
266 try writer.writeAll(" Only the preprocessor will be invoked\n");
267 }
268 if (self.symbols.count() > 0) {
269 try writer.writeAll(" Symbols:\n");
270 var it = self.symbols.iterator();
271 while (it.next()) |symbol| {
272 try writer.print(" {s} {s}", .{ switch (symbol.value_ptr.*) {
273 .define => "#define",
274 .undefine => "#undef",
275 }, symbol.key_ptr.* });
276 if (symbol.value_ptr.* == .define) {
277 try writer.print(" {s}", .{symbol.value_ptr.define});
278 }
279 try writer.writeAll("\n");
280 }
281 }
282 if (self.null_terminate_string_table_strings) {
283 try writer.writeAll(" Strings in string tables will be null-terminated\n");
284 }
285 if (self.max_string_literal_codepoints != lex.default_max_string_literal_codepoints) {
286 try writer.print(" Max string literal length: {}\n", .{self.max_string_literal_codepoints});
287 }
288 if (self.silent_duplicate_control_ids) {
289 try writer.writeAll(" Duplicate control IDs will not emit warnings\n");
290 }
291 if (self.silent_duplicate_control_ids) {
292 try writer.writeAll(" Invalid code page in .rc will produce a warning (instead of an error)\n");
293 }
294
295 const language_id = self.default_language_id orelse res.Language.default;
296 const language_name = language_name: {
297 if (std.meta.intToEnum(lang.LanguageId, language_id)) |lang_enum_val| {
298 break :language_name @tagName(lang_enum_val);
299 } else |_| {}
300 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
301 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
302 }
303 break :language_name "<UNKNOWN>";
304 };
305 try writer.print("Default language: {s} (id=0x{x})\n", .{ language_name, language_id });
306
307 const code_page = self.default_code_page orelse .windows1252;
308 try writer.print("Default codepage: {s} (id={})\n", .{ @tagName(code_page), @intFromEnum(code_page) });
309 }
310};
311
312pub const Arg = struct {
313 prefix: enum { long, short, slash },
314 name_offset: usize,
315 full: []const u8,
316
317 pub fn fromString(str: []const u8) ?@This() {
318 if (std.mem.startsWith(u8, str, "--")) {
319 return .{ .prefix = .long, .name_offset = 2, .full = str };
320 } else if (std.mem.startsWith(u8, str, "-")) {
321 return .{ .prefix = .short, .name_offset = 1, .full = str };
322 } else if (std.mem.startsWith(u8, str, "/")) {
323 return .{ .prefix = .slash, .name_offset = 1, .full = str };
324 }
325 return null;
326 }
327
328 pub fn prefixSlice(self: Arg) []const u8 {
329 return self.full[0..(if (self.prefix == .long) 2 else 1)];
330 }
331
332 pub fn name(self: Arg) []const u8 {
333 return self.full[self.name_offset..];
334 }
335
336 pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 {
337 return self.name()[0..option_len];
338 }
339
340 pub fn missingSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
341 return .{
342 .point_at_next_arg = true,
343 .value_offset = 0,
344 .name_offset = self.name_offset,
345 .prefix_len = self.prefixSlice().len,
346 };
347 }
348
349 pub fn optionAndAfterSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
350 return self.optionSpan(0);
351 }
352
353 pub fn optionSpan(self: Arg, option_len: usize) Diagnostics.ErrorDetails.ArgSpan {
354 return .{
355 .name_offset = self.name_offset,
356 .prefix_len = self.prefixSlice().len,
357 .name_len = option_len,
358 };
359 }
360
361 pub const Value = struct {
362 slice: []const u8,
363 index_increment: u2 = 1,
364
365 pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan {
366 const prefix_len = arg.prefixSlice().len;
367 switch (self.index_increment) {
368 1 => return .{
369 .value_offset = @intFromPtr(self.slice.ptr) - @intFromPtr(arg.full.ptr),
370 .prefix_len = prefix_len,
371 .name_offset = arg.name_offset,
372 },
373 2 => return .{
374 .point_at_next_arg = true,
375 .prefix_len = prefix_len,
376 .name_offset = arg.name_offset,
377 },
378 else => unreachable,
379 }
380 }
381
382 pub fn index(self: Value, arg_index: usize) usize {
383 if (self.index_increment == 2) return arg_index + 1;
384 return arg_index;
385 }
386 };
387
388 pub fn value(self: Arg, option_len: usize, index: usize, args: []const []const u8) error{MissingValue}!Value {
389 const rest = self.full[self.name_offset + option_len ..];
390 if (rest.len > 0) return .{ .slice = rest };
391 if (index + 1 >= args.len) return error.MissingValue;
392 return .{ .slice = args[index + 1], .index_increment = 2 };
393 }
394
395 pub const Context = struct {
396 index: usize,
397 arg: Arg,
398 value: Value,
399 };
400};
401
402pub const ParseError = error{ParseError} || Allocator.Error;
403
404/// Note: Does not run `Options.maybeAppendRC` automatically. If that behavior is desired,
405/// it must be called separately.
406pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options {
407 var options = Options{ .allocator = allocator };
408 errdefer options.deinit();
409
410 var output_filename: ?[]const u8 = null;
411 var output_filename_context: Arg.Context = undefined;
412
413 var arg_i: usize = 0;
414 next_arg: while (arg_i < args.len) {
415 var arg = Arg.fromString(args[arg_i]) orelse break;
416 if (arg.name().len == 0) {
417 switch (arg.prefix) {
418 // -- on its own ends arg parsing
419 .long => {
420 arg_i += 1;
421 break;
422 },
423 // - or / on its own is an error
424 else => {
425 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
426 var msg_writer = err_details.msg.writer(allocator);
427 try msg_writer.print("invalid option: {s}", .{arg.prefixSlice()});
428 try diagnostics.append(err_details);
429 arg_i += 1;
430 continue :next_arg;
431 },
432 }
433 }
434
435 while (arg.name().len > 0) {
436 const arg_name = arg.name();
437 // Note: These cases should be in order from longest to shortest, since
438 // shorter options that are a substring of a longer one could make
439 // the longer option's branch unreachable.
440 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
441 options.preprocess = .no;
442 arg.name_offset += ":no-preprocess".len;
443 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":mingw-includes")) {
444 const value = arg.value(":mingw-includes".len, arg_i, args) catch {
445 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
446 var msg_writer = err_details.msg.writer(allocator);
447 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":mingw-includes".len) });
448 try diagnostics.append(err_details);
449 arg_i += 1;
450 break :next_arg;
451 };
452 if (options.mingw_includes_dir) |overwritten_path| {
453 allocator.free(overwritten_path);
454 options.mingw_includes_dir = null;
455 }
456 const path = try allocator.dupe(u8, value.slice);
457 errdefer allocator.free(path);
458 options.mingw_includes_dir = path;
459 arg_i += value.index_increment;
460 continue :next_arg;
461 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
462 const value = arg.value(":auto-includes".len, arg_i, args) catch {
463 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
464 var msg_writer = err_details.msg.writer(allocator);
465 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":auto-includes".len) });
466 try diagnostics.append(err_details);
467 arg_i += 1;
468 break :next_arg;
469 };
470 options.auto_includes = std.meta.stringToEnum(Options.AutoIncludes, value.slice) orelse blk: {
471 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
472 var msg_writer = err_details.msg.writer(allocator);
473 try msg_writer.print("invalid auto includes setting: {s} ", .{value.slice});
474 try diagnostics.append(err_details);
475 break :blk options.auto_includes;
476 };
477 arg_i += value.index_increment;
478 continue :next_arg;
479 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) {
480 const value = arg.value(":depfile-fmt".len, arg_i, args) catch {
481 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
482 var msg_writer = err_details.msg.writer(allocator);
483 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile-fmt".len) });
484 try diagnostics.append(err_details);
485 arg_i += 1;
486 break :next_arg;
487 };
488 options.depfile_fmt = std.meta.stringToEnum(Options.DepfileFormat, value.slice) orelse blk: {
489 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
490 var msg_writer = err_details.msg.writer(allocator);
491 try msg_writer.print("invalid depfile format setting: {s} ", .{value.slice});
492 try diagnostics.append(err_details);
493 break :blk options.depfile_fmt;
494 };
495 arg_i += value.index_increment;
496 continue :next_arg;
497 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile")) {
498 const value = arg.value(":depfile".len, arg_i, args) catch {
499 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
500 var msg_writer = err_details.msg.writer(allocator);
501 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile".len) });
502 try diagnostics.append(err_details);
503 arg_i += 1;
504 break :next_arg;
505 };
506 if (options.depfile_path) |overwritten_path| {
507 allocator.free(overwritten_path);
508 options.depfile_path = null;
509 }
510 const path = try allocator.dupe(u8, value.slice);
511 errdefer allocator.free(path);
512 options.depfile_path = path;
513 arg_i += value.index_increment;
514 continue :next_arg;
515 } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
516 // No-op, we don't display any 'logo' to suppress
517 arg.name_offset += "nologo".len;
518 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":debug")) {
519 options.debug = true;
520 arg.name_offset += ":debug".len;
521 }
522 // Unsupported LCX/LCE options that need a value (within the same arg only)
523 else if (std.ascii.startsWithIgnoreCase(arg_name, "tp:")) {
524 const rest = arg.full[arg.name_offset + 3 ..];
525 if (rest.len == 0) {
526 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = .{
527 .name_offset = arg.name_offset,
528 .prefix_len = arg.prefixSlice().len,
529 .value_offset = arg.name_offset + 3,
530 } };
531 var msg_writer = err_details.msg.writer(allocator);
532 try msg_writer.print("missing value for {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
533 try diagnostics.append(err_details);
534 }
535 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
536 var msg_writer = err_details.msg.writer(allocator);
537 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
538 try diagnostics.append(err_details);
539 arg_i += 1;
540 continue :next_arg;
541 }
542 // Unsupported LCX/LCE options that need a value
543 else if (std.ascii.startsWithIgnoreCase(arg_name, "tn")) {
544 const value = arg.value(2, arg_i, args) catch no_value: {
545 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
546 var msg_writer = err_details.msg.writer(allocator);
547 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
548 try diagnostics.append(err_details);
549 // dummy zero-length slice starting where the value would have been
550 const value_start = arg.name_offset + 2;
551 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
552 };
553 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
554 var msg_writer = err_details.msg.writer(allocator);
555 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
556 try diagnostics.append(err_details);
557 arg_i += value.index_increment;
558 continue :next_arg;
559 }
560 // Unsupported MUI options that need a value
561 else if (std.ascii.startsWithIgnoreCase(arg_name, "fm") or
562 std.ascii.startsWithIgnoreCase(arg_name, "gn") or
563 std.ascii.startsWithIgnoreCase(arg_name, "g2"))
564 {
565 const value = arg.value(2, arg_i, args) catch no_value: {
566 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
567 var msg_writer = err_details.msg.writer(allocator);
568 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
569 try diagnostics.append(err_details);
570 // dummy zero-length slice starting where the value would have been
571 const value_start = arg.name_offset + 2;
572 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
573 };
574 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
575 var msg_writer = err_details.msg.writer(allocator);
576 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
577 try diagnostics.append(err_details);
578 arg_i += value.index_increment;
579 continue :next_arg;
580 }
581 // Unsupported MUI options that do not need a value
582 else if (std.ascii.startsWithIgnoreCase(arg_name, "g1")) {
583 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
584 var msg_writer = err_details.msg.writer(allocator);
585 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
586 try diagnostics.append(err_details);
587 arg.name_offset += 2;
588 }
589 // Unsupported LCX/LCE options that do not need a value
590 else if (std.ascii.startsWithIgnoreCase(arg_name, "tm") or
591 std.ascii.startsWithIgnoreCase(arg_name, "tc") or
592 std.ascii.startsWithIgnoreCase(arg_name, "tw") or
593 std.ascii.startsWithIgnoreCase(arg_name, "te") or
594 std.ascii.startsWithIgnoreCase(arg_name, "ti") or
595 std.ascii.startsWithIgnoreCase(arg_name, "ta"))
596 {
597 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
598 var msg_writer = err_details.msg.writer(allocator);
599 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
600 try diagnostics.append(err_details);
601 arg.name_offset += 2;
602 } else if (std.ascii.startsWithIgnoreCase(arg_name, "fo")) {
603 const value = arg.value(2, arg_i, args) catch {
604 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
605 var msg_writer = err_details.msg.writer(allocator);
606 try msg_writer.print("missing output path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
607 try diagnostics.append(err_details);
608 arg_i += 1;
609 break :next_arg;
610 };
611 output_filename_context = .{ .index = arg_i, .arg = arg, .value = value };
612 output_filename = value.slice;
613 arg_i += value.index_increment;
614 continue :next_arg;
615 } else if (std.ascii.startsWithIgnoreCase(arg_name, "sl")) {
616 const value = arg.value(2, arg_i, args) catch {
617 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
618 var msg_writer = err_details.msg.writer(allocator);
619 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
620 try diagnostics.append(err_details);
621 arg_i += 1;
622 break :next_arg;
623 };
624 const percent_str = value.slice;
625 const percent: u32 = parsePercent(percent_str) catch {
626 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
627 var msg_writer = err_details.msg.writer(allocator);
628 try msg_writer.print("invalid percent format '{s}'", .{percent_str});
629 try diagnostics.append(err_details);
630 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
631 var note_writer = note_details.msg.writer(allocator);
632 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
633 try diagnostics.append(note_details);
634 arg_i += value.index_increment;
635 continue :next_arg;
636 };
637 if (percent == 0 or percent > 100) {
638 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
639 var msg_writer = err_details.msg.writer(allocator);
640 try msg_writer.print("percent out of range: {} (parsed from '{s}')", .{ percent, percent_str });
641 try diagnostics.append(err_details);
642 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
643 var note_writer = note_details.msg.writer(allocator);
644 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
645 try diagnostics.append(note_details);
646 arg_i += value.index_increment;
647 continue :next_arg;
648 }
649 const percent_float = @as(f32, @floatFromInt(percent)) / 100;
650 options.max_string_literal_codepoints = @intFromFloat(percent_float * max_string_literal_length_100_percent);
651 arg_i += value.index_increment;
652 continue :next_arg;
653 } else if (std.ascii.startsWithIgnoreCase(arg_name, "ln")) {
654 const value = arg.value(2, arg_i, args) catch {
655 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
656 var msg_writer = err_details.msg.writer(allocator);
657 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
658 try diagnostics.append(err_details);
659 arg_i += 1;
660 break :next_arg;
661 };
662 const tag = value.slice;
663 options.default_language_id = lang.tagToInt(tag) catch {
664 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
665 var msg_writer = err_details.msg.writer(allocator);
666 try msg_writer.print("invalid language tag: {s}", .{tag});
667 try diagnostics.append(err_details);
668 arg_i += value.index_increment;
669 continue :next_arg;
670 };
671 if (options.default_language_id.? == lang.LOCALE_CUSTOM_UNSPECIFIED) {
672 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
673 var msg_writer = err_details.msg.writer(allocator);
674 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 });
675 try diagnostics.append(err_details);
676 }
677 arg_i += value.index_increment;
678 continue :next_arg;
679 } else if (std.ascii.startsWithIgnoreCase(arg_name, "l")) {
680 const value = arg.value(1, arg_i, args) catch {
681 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
682 var msg_writer = err_details.msg.writer(allocator);
683 try msg_writer.print("missing language ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
684 try diagnostics.append(err_details);
685 arg_i += 1;
686 break :next_arg;
687 };
688 const num_str = value.slice;
689 options.default_language_id = lang.parseInt(num_str) catch {
690 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
691 var msg_writer = err_details.msg.writer(allocator);
692 try msg_writer.print("invalid language ID: {s}", .{num_str});
693 try diagnostics.append(err_details);
694 arg_i += value.index_increment;
695 continue :next_arg;
696 };
697 arg_i += value.index_increment;
698 continue :next_arg;
699 } else if (std.ascii.startsWithIgnoreCase(arg_name, "h") or std.mem.startsWith(u8, arg_name, "?")) {
700 options.print_help_and_exit = true;
701 // If there's been an error to this point, then we still want to fail
702 if (diagnostics.hasError()) return error.ParseError;
703 return options;
704 }
705 // 1 char unsupported MUI options that need a value
706 else if (std.ascii.startsWithIgnoreCase(arg_name, "q") or
707 std.ascii.startsWithIgnoreCase(arg_name, "g"))
708 {
709 const value = arg.value(1, arg_i, args) catch no_value: {
710 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
711 var msg_writer = err_details.msg.writer(allocator);
712 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
713 try diagnostics.append(err_details);
714 // dummy zero-length slice starting where the value would have been
715 const value_start = arg.name_offset + 1;
716 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
717 };
718 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
719 var msg_writer = err_details.msg.writer(allocator);
720 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
721 try diagnostics.append(err_details);
722 arg_i += value.index_increment;
723 continue :next_arg;
724 }
725 // Undocumented (and unsupported) options that need a value
726 // /z has to do something with font substitution
727 // /s has something to do with HWB resources being inserted into the .res
728 else if (std.ascii.startsWithIgnoreCase(arg_name, "z") or
729 std.ascii.startsWithIgnoreCase(arg_name, "s"))
730 {
731 const value = arg.value(1, arg_i, args) catch no_value: {
732 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
733 var msg_writer = err_details.msg.writer(allocator);
734 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
735 try diagnostics.append(err_details);
736 // dummy zero-length slice starting where the value would have been
737 const value_start = arg.name_offset + 1;
738 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
739 };
740 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
741 var msg_writer = err_details.msg.writer(allocator);
742 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
743 try diagnostics.append(err_details);
744 arg_i += value.index_increment;
745 continue :next_arg;
746 }
747 // 1 char unsupported LCX/LCE options that do not need a value
748 else if (std.ascii.startsWithIgnoreCase(arg_name, "t")) {
749 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
750 var msg_writer = err_details.msg.writer(allocator);
751 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
752 try diagnostics.append(err_details);
753 arg.name_offset += 1;
754 } else if (std.ascii.startsWithIgnoreCase(arg_name, "c")) {
755 const value = arg.value(1, arg_i, args) catch {
756 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
757 var msg_writer = err_details.msg.writer(allocator);
758 try msg_writer.print("missing code page ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
759 try diagnostics.append(err_details);
760 arg_i += 1;
761 break :next_arg;
762 };
763 const num_str = value.slice;
764 const code_page_id = std.fmt.parseUnsigned(u16, num_str, 10) catch {
765 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
766 var msg_writer = err_details.msg.writer(allocator);
767 try msg_writer.print("invalid code page ID: {s}", .{num_str});
768 try diagnostics.append(err_details);
769 arg_i += value.index_increment;
770 continue :next_arg;
771 };
772 options.default_code_page = CodePage.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {
773 error.InvalidCodePage => {
774 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
775 var msg_writer = err_details.msg.writer(allocator);
776 try msg_writer.print("invalid or unknown code page ID: {}", .{code_page_id});
777 try diagnostics.append(err_details);
778 arg_i += value.index_increment;
779 continue :next_arg;
780 },
781 error.UnsupportedCodePage => {
782 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
783 var msg_writer = err_details.msg.writer(allocator);
784 try msg_writer.print("unsupported code page: {s} (id={})", .{
785 @tagName(CodePage.getByIdentifier(code_page_id) catch unreachable),
786 code_page_id,
787 });
788 try diagnostics.append(err_details);
789 arg_i += value.index_increment;
790 continue :next_arg;
791 },
792 };
793 arg_i += value.index_increment;
794 continue :next_arg;
795 } else if (std.ascii.startsWithIgnoreCase(arg_name, "v")) {
796 options.verbose = true;
797 arg.name_offset += 1;
798 } else if (std.ascii.startsWithIgnoreCase(arg_name, "x")) {
799 options.ignore_include_env_var = true;
800 arg.name_offset += 1;
801 } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) {
802 options.preprocess = .only;
803 arg.name_offset += 1;
804 } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) {
805 const value = arg.value(1, arg_i, args) catch {
806 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
807 var msg_writer = err_details.msg.writer(allocator);
808 try msg_writer.print("missing include path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
809 try diagnostics.append(err_details);
810 arg_i += 1;
811 break :next_arg;
812 };
813 const path = value.slice;
814 const duped = try allocator.dupe(u8, path);
815 errdefer allocator.free(duped);
816 try options.extra_include_paths.append(options.allocator, duped);
817 arg_i += value.index_increment;
818 continue :next_arg;
819 } else if (std.ascii.startsWithIgnoreCase(arg_name, "r")) {
820 // From https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
821 // "Ignored. Provided for compatibility with existing makefiles."
822 arg.name_offset += 1;
823 } else if (std.ascii.startsWithIgnoreCase(arg_name, "n")) {
824 options.null_terminate_string_table_strings = true;
825 arg.name_offset += 1;
826 } else if (std.ascii.startsWithIgnoreCase(arg_name, "y")) {
827 options.silent_duplicate_control_ids = true;
828 arg.name_offset += 1;
829 } else if (std.ascii.startsWithIgnoreCase(arg_name, "w")) {
830 options.warn_instead_of_error_on_invalid_code_page = true;
831 arg.name_offset += 1;
832 } else if (std.ascii.startsWithIgnoreCase(arg_name, "a")) {
833 // Undocumented option with unknown function
834 // TODO: More investigation to figure out what it does (if anything)
835 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
836 var msg_writer = err_details.msg.writer(allocator);
837 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) });
838 try diagnostics.append(err_details);
839 arg.name_offset += 1;
840 } else if (std.ascii.startsWithIgnoreCase(arg_name, "d")) {
841 const value = arg.value(1, arg_i, args) catch {
842 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
843 var msg_writer = err_details.msg.writer(allocator);
844 try msg_writer.print("missing symbol to define after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
845 try diagnostics.append(err_details);
846 arg_i += 1;
847 break :next_arg;
848 };
849 var tokenizer = std.mem.tokenize(u8, value.slice, "=");
850 // guaranteed to exist since an empty value.slice would invoke
851 // the 'missing symbol to define' branch above
852 const symbol = tokenizer.next().?;
853 const symbol_value = tokenizer.next() orelse "1";
854
855 if (isValidIdentifier(symbol)) {
856 try options.define(symbol, symbol_value);
857 } else {
858 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
859 var msg_writer = err_details.msg.writer(allocator);
860 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be defined", .{symbol});
861 try diagnostics.append(err_details);
862 }
863 arg_i += value.index_increment;
864 continue :next_arg;
865 } else if (std.ascii.startsWithIgnoreCase(arg_name, "u")) {
866 const value = arg.value(1, arg_i, args) catch {
867 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
868 var msg_writer = err_details.msg.writer(allocator);
869 try msg_writer.print("missing symbol to undefine after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
870 try diagnostics.append(err_details);
871 arg_i += 1;
872 break :next_arg;
873 };
874 const symbol = value.slice;
875 if (isValidIdentifier(symbol)) {
876 try options.undefine(symbol);
877 } else {
878 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
879 var msg_writer = err_details.msg.writer(allocator);
880 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be undefined", .{symbol});
881 try diagnostics.append(err_details);
882 }
883 arg_i += value.index_increment;
884 continue :next_arg;
885 } else {
886 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
887 var msg_writer = err_details.msg.writer(allocator);
888 try msg_writer.print("invalid option: {s}{s}", .{ arg.prefixSlice(), arg.name() });
889 try diagnostics.append(err_details);
890 arg_i += 1;
891 continue :next_arg;
892 }
893 } else {
894 // The while loop exited via its conditional, meaning we are done with
895 // the current arg and can move on the the next
896 arg_i += 1;
897 continue;
898 }
899 }
900
901 const positionals = args[arg_i..];
902
903 if (positionals.len < 1) {
904 var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };
905 var msg_writer = err_details.msg.writer(allocator);
906 try msg_writer.writeAll("missing input filename");
907 try diagnostics.append(err_details);
908
909 const last_arg = args[args.len - 1];
910 if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {
911 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
912 var note_writer = note_details.msg.writer(allocator);
913 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");
914 try diagnostics.append(note_details);
915 }
916
917 // This is a fatal enough problem to justify an early return, since
918 // things after this rely on the value of the input filename.
919 return error.ParseError;
920 }
921 options.input_filename = try allocator.dupe(u8, positionals[0]);
922
923 if (positionals.len > 1) {
924 if (output_filename != null) {
925 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i + 1 };
926 var msg_writer = err_details.msg.writer(allocator);
927 try msg_writer.writeAll("output filename already specified");
928 try diagnostics.append(err_details);
929 var note_details = Diagnostics.ErrorDetails{
930 .type = .note,
931 .arg_index = output_filename_context.value.index(output_filename_context.index),
932 .arg_span = output_filename_context.value.argSpan(output_filename_context.arg),
933 };
934 var note_writer = note_details.msg.writer(allocator);
935 try note_writer.writeAll("output filename previously specified here");
936 try diagnostics.append(note_details);
937 } else {
938 output_filename = positionals[1];
939 }
940 }
941 if (output_filename == null) {
942 var buf = std.ArrayList(u8).init(allocator);
943 errdefer buf.deinit();
944
945 if (std.fs.path.dirname(options.input_filename)) |dirname| {
946 var end_pos = dirname.len;
947 // We want to ensure that we write a path separator at the end, so if the dirname
948 // doesn't end with a path sep then include the char after the dirname
949 // which must be a path sep.
950 if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1;
951 try buf.appendSlice(options.input_filename[0..end_pos]);
952 }
953 try buf.appendSlice(std.fs.path.stem(options.input_filename));
954 if (options.preprocess == .only) {
955 try buf.appendSlice(".rcpp");
956 } else {
957 try buf.appendSlice(".res");
958 }
959
960 options.output_filename = try buf.toOwnedSlice();
961 } else {
962 options.output_filename = try allocator.dupe(u8, output_filename.?);
963 }
964
965 if (diagnostics.hasError()) {
966 return error.ParseError;
967 }
968
969 return options;
970}
971
972/// Returns true if the str is a valid C identifier for use in a #define/#undef macro
973pub fn isValidIdentifier(str: []const u8) bool {
974 for (str, 0..) |c, i| switch (c) {
975 '0'...'9' => if (i == 0) return false,
976 'a'...'z', 'A'...'Z', '_' => {},
977 else => return false,
978 };
979 return true;
980}
981
982/// This function is specific to how the Win32 RC command line interprets
983/// max string literal length percent.
984/// - Wraps on overflow of u32
985/// - Stops parsing on any invalid hexadecimal digits
986/// - Errors if a digit is not the first char
987/// - `-` (negative) prefix is allowed
988pub fn parsePercent(str: []const u8) error{InvalidFormat}!u32 {
989 var result: u32 = 0;
990 const radix: u8 = 10;
991 var buf = str;
992
993 const Prefix = enum { none, minus };
994 var prefix: Prefix = .none;
995 switch (buf[0]) {
996 '-' => {
997 prefix = .minus;
998 buf = buf[1..];
999 },
1000 else => {},
1001 }
1002
1003 for (buf, 0..) |c, i| {
1004 const digit = switch (c) {
1005 // On invalid digit for the radix, just stop parsing but don't fail
1006 '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
1007 else => {
1008 // First digit must be valid
1009 if (i == 0) {
1010 return error.InvalidFormat;
1011 }
1012 break;
1013 },
1014 };
1015
1016 if (result != 0) {
1017 result *%= radix;
1018 }
1019 result +%= digit;
1020 }
1021
1022 switch (prefix) {
1023 .none => {},
1024 .minus => result = 0 -% result,
1025 }
1026
1027 return result;
1028}
1029
1030test parsePercent {
1031 try std.testing.expectEqual(@as(u32, 16), try parsePercent("16"));
1032 try std.testing.expectEqual(@as(u32, 0), try parsePercent("0x1A"));
1033 try std.testing.expectEqual(@as(u32, 0x1), try parsePercent("1zzzz"));
1034 try std.testing.expectEqual(@as(u32, 0xffffffff), try parsePercent("-1"));
1035 try std.testing.expectEqual(@as(u32, 0xfffffff0), try parsePercent("-16"));
1036 try std.testing.expectEqual(@as(u32, 1), try parsePercent("4294967297"));
1037 try std.testing.expectError(error.InvalidFormat, parsePercent("--1"));
1038 try std.testing.expectError(error.InvalidFormat, parsePercent("ha"));
1039 try std.testing.expectError(error.InvalidFormat, parsePercent("¹"));
1040 try std.testing.expectError(error.InvalidFormat, parsePercent("~1"));
1041}
1042
1043pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, err_details: Diagnostics.ErrorDetails, args: []const []const u8) !void {
1044 try config.setColor(writer, .dim);
1045 try writer.writeAll("<cli>");
1046 try config.setColor(writer, .reset);
1047 try config.setColor(writer, .bold);
1048 try writer.writeAll(": ");
1049 switch (err_details.type) {
1050 .err => {
1051 try config.setColor(writer, .red);
1052 try writer.writeAll("error: ");
1053 },
1054 .warning => {
1055 try config.setColor(writer, .yellow);
1056 try writer.writeAll("warning: ");
1057 },
1058 .note => {
1059 try config.setColor(writer, .cyan);
1060 try writer.writeAll("note: ");
1061 },
1062 }
1063 try config.setColor(writer, .reset);
1064 try config.setColor(writer, .bold);
1065 try writer.writeAll(err_details.msg.items);
1066 try writer.writeByte('\n');
1067 try config.setColor(writer, .reset);
1068
1069 if (!err_details.print_args) {
1070 try writer.writeByte('\n');
1071 return;
1072 }
1073
1074 try config.setColor(writer, .dim);
1075 const prefix = " ... ";
1076 try writer.writeAll(prefix);
1077 try config.setColor(writer, .reset);
1078
1079 const arg_with_name = args[err_details.arg_index];
1080 const prefix_slice = arg_with_name[0..err_details.arg_span.prefix_len];
1081 const before_name_slice = arg_with_name[err_details.arg_span.prefix_len..err_details.arg_span.name_offset];
1082 var name_slice = arg_with_name[err_details.arg_span.name_offset..];
1083 if (err_details.arg_span.name_len > 0) name_slice.len = err_details.arg_span.name_len;
1084 const after_name_slice = arg_with_name[err_details.arg_span.name_offset + name_slice.len ..];
1085
1086 try writer.writeAll(prefix_slice);
1087 if (before_name_slice.len > 0) {
1088 try config.setColor(writer, .dim);
1089 try writer.writeAll(before_name_slice);
1090 try config.setColor(writer, .reset);
1091 }
1092 try writer.writeAll(name_slice);
1093 if (after_name_slice.len > 0) {
1094 try config.setColor(writer, .dim);
1095 try writer.writeAll(after_name_slice);
1096 try config.setColor(writer, .reset);
1097 }
1098
1099 var next_arg_len: usize = 0;
1100 if (err_details.arg_span.point_at_next_arg and err_details.arg_index + 1 < args.len) {
1101 const next_arg = args[err_details.arg_index + 1];
1102 try writer.writeByte(' ');
1103 try writer.writeAll(next_arg);
1104 next_arg_len = next_arg.len;
1105 }
1106
1107 const last_shown_arg_index = if (err_details.arg_span.point_at_next_arg) err_details.arg_index + 1 else err_details.arg_index;
1108 if (last_shown_arg_index + 1 < args.len) {
1109 // special case for when pointing to a missing value within the same arg
1110 // as the name
1111 if (err_details.arg_span.value_offset >= arg_with_name.len) {
1112 try writer.writeByte(' ');
1113 }
1114 try config.setColor(writer, .dim);
1115 try writer.writeAll(" ...");
1116 try config.setColor(writer, .reset);
1117 }
1118 try writer.writeByte('\n');
1119
1120 try config.setColor(writer, .green);
1121 try writer.writeByteNTimes(' ', prefix.len);
1122 // Special case for when the option is *only* a prefix (e.g. invalid option: -)
1123 if (err_details.arg_span.prefix_len == arg_with_name.len) {
1124 try writer.writeByteNTimes('^', err_details.arg_span.prefix_len);
1125 } else {
1126 try writer.writeByteNTimes('~', err_details.arg_span.prefix_len);
1127 try writer.writeByteNTimes(' ', err_details.arg_span.name_offset - err_details.arg_span.prefix_len);
1128 if (!err_details.arg_span.point_at_next_arg and err_details.arg_span.value_offset == 0) {
1129 try writer.writeByte('^');
1130 try writer.writeByteNTimes('~', name_slice.len - 1);
1131 } else if (err_details.arg_span.value_offset > 0) {
1132 try writer.writeByteNTimes('~', err_details.arg_span.value_offset - err_details.arg_span.name_offset);
1133 try writer.writeByte('^');
1134 if (err_details.arg_span.value_offset < arg_with_name.len) {
1135 try writer.writeByteNTimes('~', arg_with_name.len - err_details.arg_span.value_offset - 1);
1136 }
1137 } else if (err_details.arg_span.point_at_next_arg) {
1138 try writer.writeByteNTimes('~', arg_with_name.len - err_details.arg_span.name_offset + 1);
1139 try writer.writeByte('^');
1140 if (next_arg_len > 0) {
1141 try writer.writeByteNTimes('~', next_arg_len - 1);
1142 }
1143 }
1144 }
1145 try writer.writeByte('\n');
1146 try config.setColor(writer, .reset);
1147}
1148
1149fn testParse(args: []const []const u8) !Options {
1150 return (try testParseOutput(args, "")).?;
1151}
1152
1153fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options {
1154 return (try testParseOutput(args, expected_output)).?;
1155}
1156
1157fn testParseError(args: []const []const u8, expected_output: []const u8) !void {
1158 var maybe_options = try testParseOutput(args, expected_output);
1159 if (maybe_options != null) {
1160 std.debug.print("expected error, got options: {}\n", .{maybe_options.?});
1161 maybe_options.?.deinit();
1162 return error.TestExpectedError;
1163 }
1164}
1165
1166fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Options {
1167 var diagnostics = Diagnostics.init(std.testing.allocator);
1168 defer diagnostics.deinit();
1169
1170 var output = std.ArrayList(u8).init(std.testing.allocator);
1171 defer output.deinit();
1172
1173 var options = parse(std.testing.allocator, args, &diagnostics) catch |err| switch (err) {
1174 error.ParseError => {
1175 try diagnostics.renderToWriter(args, output.writer(), .no_color);
1176 try std.testing.expectEqualStrings(expected_output, output.items);
1177 return null;
1178 },
1179 else => |e| return e,
1180 };
1181 errdefer options.deinit();
1182
1183 try diagnostics.renderToWriter(args, output.writer(), .no_color);
1184 try std.testing.expectEqualStrings(expected_output, output.items);
1185 return options;
1186}
1187
1188test "parse errors: basic" {
1189 try testParseError(&.{"/"},
1190 \\<cli>: error: invalid option: /
1191 \\ ... /
1192 \\ ^
1193 \\<cli>: error: missing input filename
1194 \\
1195 \\
1196 );
1197 try testParseError(&.{"/ln"},
1198 \\<cli>: error: missing language tag after /ln option
1199 \\ ... /ln
1200 \\ ~~~~^
1201 \\<cli>: error: missing input filename
1202 \\
1203 \\
1204 );
1205 try testParseError(&.{"-vln"},
1206 \\<cli>: error: missing language tag after -ln option
1207 \\ ... -vln
1208 \\ ~ ~~~^
1209 \\<cli>: error: missing input filename
1210 \\
1211 \\
1212 );
1213 try testParseError(&.{"/_not-an-option"},
1214 \\<cli>: error: invalid option: /_not-an-option
1215 \\ ... /_not-an-option
1216 \\ ~^~~~~~~~~~~~~~
1217 \\<cli>: error: missing input filename
1218 \\
1219 \\
1220 );
1221 try testParseError(&.{"-_not-an-option"},
1222 \\<cli>: error: invalid option: -_not-an-option
1223 \\ ... -_not-an-option
1224 \\ ~^~~~~~~~~~~~~~
1225 \\<cli>: error: missing input filename
1226 \\
1227 \\
1228 );
1229 try testParseError(&.{"--_not-an-option"},
1230 \\<cli>: error: invalid option: --_not-an-option
1231 \\ ... --_not-an-option
1232 \\ ~~^~~~~~~~~~~~~~
1233 \\<cli>: error: missing input filename
1234 \\
1235 \\
1236 );
1237 try testParseError(&.{"/v_not-an-option"},
1238 \\<cli>: error: invalid option: /_not-an-option
1239 \\ ... /v_not-an-option
1240 \\ ~ ^~~~~~~~~~~~~~
1241 \\<cli>: error: missing input filename
1242 \\
1243 \\
1244 );
1245 try testParseError(&.{"-v_not-an-option"},
1246 \\<cli>: error: invalid option: -_not-an-option
1247 \\ ... -v_not-an-option
1248 \\ ~ ^~~~~~~~~~~~~~
1249 \\<cli>: error: missing input filename
1250 \\
1251 \\
1252 );
1253 try testParseError(&.{"--v_not-an-option"},
1254 \\<cli>: error: invalid option: --_not-an-option
1255 \\ ... --v_not-an-option
1256 \\ ~~ ^~~~~~~~~~~~~~
1257 \\<cli>: error: missing input filename
1258 \\
1259 \\
1260 );
1261 try testParseError(&.{"/some/absolute/path/parsed/as/an/option.rc"},
1262 \\<cli>: error: the /s option is unsupported
1263 \\ ... /some/absolute/path/parsed/as/an/option.rc
1264 \\ ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1265 \\<cli>: error: missing input filename
1266 \\
1267 \\<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
1268 \\ ... /some/absolute/path/parsed/as/an/option.rc
1269 \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1270 \\
1271 );
1272}
1273
1274test "parse errors: /ln" {
1275 try testParseError(&.{ "/ln", "invalid", "foo.rc" },
1276 \\<cli>: error: invalid language tag: invalid
1277 \\ ... /ln invalid ...
1278 \\ ~~~~^~~~~~~
1279 \\
1280 );
1281 try testParseError(&.{ "/lninvalid", "foo.rc" },
1282 \\<cli>: error: invalid language tag: invalid
1283 \\ ... /lninvalid ...
1284 \\ ~~~^~~~~~~
1285 \\
1286 );
1287}
1288
1289test "parse: options" {
1290 {
1291 var options = try testParse(&.{ "/v", "foo.rc" });
1292 defer options.deinit();
1293
1294 try std.testing.expectEqual(true, options.verbose);
1295 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1296 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1297 }
1298 {
1299 var options = try testParse(&.{ "/vx", "foo.rc" });
1300 defer options.deinit();
1301
1302 try std.testing.expectEqual(true, options.verbose);
1303 try std.testing.expectEqual(true, options.ignore_include_env_var);
1304 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1305 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1306 }
1307 {
1308 var options = try testParse(&.{ "/xv", "foo.rc" });
1309 defer options.deinit();
1310
1311 try std.testing.expectEqual(true, options.verbose);
1312 try std.testing.expectEqual(true, options.ignore_include_env_var);
1313 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1314 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1315 }
1316 {
1317 var options = try testParse(&.{ "/xvFObar.res", "foo.rc" });
1318 defer options.deinit();
1319
1320 try std.testing.expectEqual(true, options.verbose);
1321 try std.testing.expectEqual(true, options.ignore_include_env_var);
1322 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1323 try std.testing.expectEqualStrings("bar.res", options.output_filename);
1324 }
1325}
1326
1327test "parse: define and undefine" {
1328 {
1329 var options = try testParse(&.{ "/dfoo", "foo.rc" });
1330 defer options.deinit();
1331
1332 const action = options.symbols.get("foo").?;
1333 try std.testing.expectEqualStrings("1", action.define);
1334 }
1335 {
1336 var options = try testParse(&.{ "/dfoo=bar", "/dfoo=baz", "foo.rc" });
1337 defer options.deinit();
1338
1339 const action = options.symbols.get("foo").?;
1340 try std.testing.expectEqualStrings("baz", action.define);
1341 }
1342 {
1343 var options = try testParse(&.{ "/ufoo", "foo.rc" });
1344 defer options.deinit();
1345
1346 const action = options.symbols.get("foo").?;
1347 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1348 }
1349 {
1350 // Once undefined, future defines are ignored
1351 var options = try testParse(&.{ "/ufoo", "/dfoo", "foo.rc" });
1352 defer options.deinit();
1353
1354 const action = options.symbols.get("foo").?;
1355 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1356 }
1357 {
1358 // Undefined always takes precedence
1359 var options = try testParse(&.{ "/dfoo", "/ufoo", "/dfoo", "foo.rc" });
1360 defer options.deinit();
1361
1362 const action = options.symbols.get("foo").?;
1363 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1364 }
1365 {
1366 // Warn + ignore invalid identifiers
1367 var options = try testParseWarning(
1368 &.{ "/dfoo bar", "/u", "0leadingdigit", "foo.rc" },
1369 \\<cli>: warning: symbol "foo bar" is not a valid identifier and therefore cannot be defined
1370 \\ ... /dfoo bar ...
1371 \\ ~~^~~~~~~
1372 \\<cli>: warning: symbol "0leadingdigit" is not a valid identifier and therefore cannot be undefined
1373 \\ ... /u 0leadingdigit ...
1374 \\ ~~~^~~~~~~~~~~~~
1375 \\
1376 ,
1377 );
1378 defer options.deinit();
1379
1380 try std.testing.expectEqual(@as(usize, 0), options.symbols.count());
1381 }
1382}
1383
1384test "parse: /sl" {
1385 try testParseError(&.{ "/sl", "0", "foo.rc" },
1386 \\<cli>: error: percent out of range: 0 (parsed from '0')
1387 \\ ... /sl 0 ...
1388 \\ ~~~~^
1389 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1390 \\
1391 \\
1392 );
1393 try testParseError(&.{ "/sl", "abcd", "foo.rc" },
1394 \\<cli>: error: invalid percent format 'abcd'
1395 \\ ... /sl abcd ...
1396 \\ ~~~~^~~~
1397 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1398 \\
1399 \\
1400 );
1401 {
1402 var options = try testParse(&.{"foo.rc"});
1403 defer options.deinit();
1404
1405 try std.testing.expectEqual(@as(u15, lex.default_max_string_literal_codepoints), options.max_string_literal_codepoints);
1406 }
1407 {
1408 var options = try testParse(&.{ "/sl100", "foo.rc" });
1409 defer options.deinit();
1410
1411 try std.testing.expectEqual(@as(u15, max_string_literal_length_100_percent), options.max_string_literal_codepoints);
1412 }
1413 {
1414 var options = try testParse(&.{ "-SL33", "foo.rc" });
1415 defer options.deinit();
1416
1417 try std.testing.expectEqual(@as(u15, 2703), options.max_string_literal_codepoints);
1418 }
1419 {
1420 var options = try testParse(&.{ "/sl15", "foo.rc" });
1421 defer options.deinit();
1422
1423 try std.testing.expectEqual(@as(u15, 1228), options.max_string_literal_codepoints);
1424 }
1425}
1426
1427test "parse: unsupported MUI-related options" {
1428 try testParseError(&.{ "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" },
1429 \\<cli>: error: the /q option is unsupported
1430 \\ ... /q ...
1431 \\ ~^
1432 \\<cli>: error: the /g1 option is unsupported
1433 \\ ... /g1 ...
1434 \\ ~^~
1435 \\<cli>: error: the -G2 option is unsupported
1436 \\ ... -G2 ...
1437 \\ ~^~
1438 \\<cli>: error: the /fm option is unsupported
1439 \\ ... /fm ...
1440 \\ ~^~
1441 \\<cli>: error: the /g option is unsupported
1442 \\ ... /g ...
1443 \\ ~^
1444 \\
1445 );
1446}
1447
1448test "parse: unsupported LCX/LCE-related options" {
1449 try testParseError(&.{ "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" },
1450 \\<cli>: error: the /t option is unsupported
1451 \\ ... /t ...
1452 \\ ~^
1453 \\<cli>: error: missing value for /tp: option
1454 \\ ... /tp: ...
1455 \\ ~~~~^
1456 \\<cli>: error: the /tp: option is unsupported
1457 \\ ... /tp: ...
1458 \\ ~^~~
1459 \\<cli>: error: the /tp: option is unsupported
1460 \\ ... /tp:blah ...
1461 \\ ~^~~~~~~
1462 \\<cli>: error: the /tm option is unsupported
1463 \\ ... /tm ...
1464 \\ ~^~
1465 \\<cli>: error: the /tc option is unsupported
1466 \\ ... /tc ...
1467 \\ ~^~
1468 \\<cli>: error: the /tw option is unsupported
1469 \\ ... /tw ...
1470 \\ ~^~
1471 \\<cli>: error: the -TE option is unsupported
1472 \\ ... -TEti ...
1473 \\ ~^~
1474 \\<cli>: error: the -ti option is unsupported
1475 \\ ... -TEti ...
1476 \\ ~ ^~
1477 \\<cli>: error: the /ta option is unsupported
1478 \\ ... /ta ...
1479 \\ ~^~
1480 \\<cli>: error: the /tn option is unsupported
1481 \\ ... /tn ...
1482 \\ ~^~
1483 \\
1484 );
1485}
1486
1487test "maybeAppendRC" {
1488 var tmp = std.testing.tmpDir(.{});
1489 defer tmp.cleanup();
1490
1491 var options = try testParse(&.{"foo"});
1492 defer options.deinit();
1493 try std.testing.expectEqualStrings("foo", options.input_filename);
1494
1495 // Create the file so that it's found. In this scenario, .rc should not get
1496 // appended.
1497 var file = try tmp.dir.createFile("foo", .{});
1498 file.close();
1499 try options.maybeAppendRC(tmp.dir);
1500 try std.testing.expectEqualStrings("foo", options.input_filename);
1501
1502 // Now delete the file and try again. Since the verbatim name is no longer found
1503 // and the input filename does not have an extension, .rc should get appended.
1504 try tmp.dir.deleteFile("foo");
1505 try options.maybeAppendRC(tmp.dir);
1506 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1507}
lib/compiler/resinator/code_pages.zig created+500
...@@ -0,0 +1,500 @@
1const std = @import("std");
2const windows1252 = @import("windows1252.zig");
3
4// TODO: Parts of this comment block may be more relevant to string/NameOrOrdinal parsing
5// than it is to the stuff in this file.
6//
7// ‰ representations for context:
8// Win-1252 89
9// UTF-8 E2 80 B0
10// UTF-16 20 30
11//
12// With code page 65001:
13// ‰ RCDATA { "‰" L"‰" }
14// File encoded as Windows-1252:
15// ‰ => <U+FFFD REPLACEMENT CHARACTER> as u16
16// "‰" => 0x3F ('?')
17// L"‰" => <U+FFFD REPLACEMENT CHARACTER> as u16
18// File encoded as UTF-8:
19// ‰ => <U+2030 ‰> as u16
20// "‰" => 0x89 ('‰' encoded as Windows-1252)
21// L"‰" => <U+2030 ‰> as u16
22//
23// With code page 1252:
24// ‰ RCDATA { "‰" L"‰" }
25// File encoded as Windows-1252:
26// ‰ => <U+2030 ‰> as u16
27// "‰" => 0x89 ('‰' encoded as Windows-1252)
28// L"‰" => <U+2030 ‰> as u16
29// File encoded as UTF-8:
30// ‰ => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16
31// ^ first byte of utf8 representation
32// ^ second byte of UTF-8 representation (0x80), but interpretted as
33// Windows-1252 ('€') and then converted to UTF-16 (<U+20AC>)
34// ^ third byte of utf8 representation
35// "‰" => 0xE2, 0x80, 0xB0 (the bytes of the UTF-8 representation)
36// L"‰" => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 (see '‰ =>' explanation)
37//
38// With code page 1252:
39// <0x90> RCDATA { "<0x90>" L"<0x90>" }
40// File encoded as Windows-1252:
41// <0x90> => 0x90 as u16
42// "<0x90>" => 0x90
43// L"<0x90>" => 0x90 as u16
44// File encoded as UTF-8:
45// <0x90> => 0xC2 as u16, 0x90 as u16
46// "<0x90>" => 0xC2, 0x90 (the bytes of the UTF-8 representation of <U+0090>)
47// L"<0x90>" => 0xC2 as u16, 0x90 as u16
48//
49// Within a raw data block, file encoded as Windows-1252 (Â is <0xC2>):
50// "Âa" L"Âa" "\xC2ad" L"\xC2AD"
51// With code page 1252:
52// C2 61 C2 00 61 00 C2 61 64 AD C2
53// Â^ a^ Â~~~^ a~~~^ .^ a^ d^ ^~~~~\xC2AD
54// \xC2~`
55// With code page 65001:
56// 3F 61 FD FF 61 00 C2 61 64 AD C2
57// ^. a^ ^~~~. a~~~^ ^. a^ d^ ^~~~~\xC2AD
58// `. `. `~\xC2
59// `. `.~<0xC2>a is not well-formed UTF-8 (0xC2 expects a continutation byte after it).
60// `. Because 'a' is a valid first byte of a UTF-8 sequence, it is not included in the
61// `. invalid sequence so only the <0xC2> gets converted to <U+FFFD>.
62// `~Same as ^ but converted to '?' instead.
63//
64// Within a raw data block, file encoded as Windows-1252 (ð is <0xF0>, € is <0x80>):
65// "ð€a" L"ð€a"
66// With code page 1252:
67// F0 80 61 F0 00 AC 20 61 00
68// ð^ €^ a^ ð~~~^ €~~~^ a~~~^
69// With code page 65001:
70// 3F 61 FD FF 61 00
71// ^. a^ ^~~~. a~~~^
72// `. `.
73// `. `.~<0xF0><0x80> is not well-formed UTF-8, and <0x80> is not a valid first byte, so
74// `. both bytes are considered an invalid sequence and get converted to '<U+FFFD>'
75// `~Same as ^ but converted to '?' instead.
76
77/// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
78pub const CodePage = enum(u16) {
79 // supported
80 windows1252 = 1252, // windows-1252 ANSI Latin 1; Western European (Windows)
81 utf8 = 65001, // utf-8 Unicode (UTF-8)
82
83 // unsupported but valid
84 ibm037 = 37, // IBM037 IBM EBCDIC US-Canada
85 ibm437 = 437, // IBM437 OEM United States
86 ibm500 = 500, // IBM500 IBM EBCDIC International
87 asmo708 = 708, // ASMO-708 Arabic (ASMO 708)
88 asmo449plus = 709, // Arabic (ASMO-449+, BCON V4)
89 transparent_arabic = 710, // Arabic - Transparent Arabic
90 dos720 = 720, // DOS-720 Arabic (Transparent ASMO); Arabic (DOS)
91 ibm737 = 737, // ibm737 OEM Greek (formerly 437G); Greek (DOS)
92 ibm775 = 775, // ibm775 OEM Baltic; Baltic (DOS)
93 ibm850 = 850, // ibm850 OEM Multilingual Latin 1; Western European (DOS)
94 ibm852 = 852, // ibm852 OEM Latin 2; Central European (DOS)
95 ibm855 = 855, // IBM855 OEM Cyrillic (primarily Russian)
96 ibm857 = 857, // ibm857 OEM Turkish; Turkish (DOS)
97 ibm00858 = 858, // IBM00858 OEM Multilingual Latin 1 + Euro symbol
98 ibm860 = 860, // IBM860 OEM Portuguese; Portuguese (DOS)
99 ibm861 = 861, // ibm861 OEM Icelandic; Icelandic (DOS)
100 dos862 = 862, // DOS-862 OEM Hebrew; Hebrew (DOS)
101 ibm863 = 863, // IBM863 OEM French Canadian; French Canadian (DOS)
102 ibm864 = 864, // IBM864 OEM Arabic; Arabic (864)
103 ibm865 = 865, // IBM865 OEM Nordic; Nordic (DOS)
104 cp866 = 866, // cp866 OEM Russian; Cyrillic (DOS)
105 ibm869 = 869, // ibm869 OEM Modern Greek; Greek, Modern (DOS)
106 ibm870 = 870, // IBM870 IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2
107 windows874 = 874, // windows-874 Thai (Windows)
108 cp875 = 875, // cp875 IBM EBCDIC Greek Modern
109 shift_jis = 932, // shift_jis ANSI/OEM Japanese; Japanese (Shift-JIS)
110 gb2312 = 936, // gb2312 ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)
111 ks_c_5601_1987 = 949, // ks_c_5601-1987 ANSI/OEM Korean (Unified Hangul Code)
112 big5 = 950, // big5 ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5)
113 ibm1026 = 1026, // IBM1026 IBM EBCDIC Turkish (Latin 5)
114 ibm01047 = 1047, // IBM01047 IBM EBCDIC Latin 1/Open System
115 ibm01140 = 1140, // IBM01140 IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro)
116 ibm01141 = 1141, // IBM01141 IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro)
117 ibm01142 = 1142, // IBM01142 IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro)
118 ibm01143 = 1143, // IBM01143 IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro)
119 ibm01144 = 1144, // IBM01144 IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro)
120 ibm01145 = 1145, // IBM01145 IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro)
121 ibm01146 = 1146, // IBM01146 IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro)
122 ibm01147 = 1147, // IBM01147 IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro)
123 ibm01148 = 1148, // IBM01148 IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro)
124 ibm01149 = 1149, // IBM01149 IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro)
125 utf16 = 1200, // utf-16 Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications
126 utf16_fffe = 1201, // unicodeFFFE Unicode UTF-16, big endian byte order; available only to managed applications
127 windows1250 = 1250, // windows-1250 ANSI Central European; Central European (Windows)
128 windows1251 = 1251, // windows-1251 ANSI Cyrillic; Cyrillic (Windows)
129 windows1253 = 1253, // windows-1253 ANSI Greek; Greek (Windows)
130 windows1254 = 1254, // windows-1254 ANSI Turkish; Turkish (Windows)
131 windows1255 = 1255, // windows-1255 ANSI Hebrew; Hebrew (Windows)
132 windows1256 = 1256, // windows-1256 ANSI Arabic; Arabic (Windows)
133 windows1257 = 1257, // windows-1257 ANSI Baltic; Baltic (Windows)
134 windows1258 = 1258, // windows-1258 ANSI/OEM Vietnamese; Vietnamese (Windows)
135 johab = 1361, // Johab Korean (Johab)
136 macintosh = 10000, // macintosh MAC Roman; Western European (Mac)
137 x_mac_japanese = 10001, // x-mac-japanese Japanese (Mac)
138 x_mac_chinesetrad = 10002, // x-mac-chinesetrad MAC Traditional Chinese (Big5); Chinese Traditional (Mac)
139 x_mac_korean = 10003, // x-mac-korean Korean (Mac)
140 x_mac_arabic = 10004, // x-mac-arabic Arabic (Mac)
141 x_mac_hebrew = 10005, // x-mac-hebrew Hebrew (Mac)
142 x_mac_greek = 10006, // x-mac-greek Greek (Mac)
143 x_mac_cyrillic = 10007, // x-mac-cyrillic Cyrillic (Mac)
144 x_mac_chinesesimp = 10008, // x-mac-chinesesimp MAC Simplified Chinese (GB 2312); Chinese Simplified (Mac)
145 x_mac_romanian = 10010, // x-mac-romanian Romanian (Mac)
146 x_mac_ukranian = 10017, // x-mac-ukrainian Ukrainian (Mac)
147 x_mac_thai = 10021, // x-mac-thai Thai (Mac)
148 x_mac_ce = 10029, // x-mac-ce MAC Latin 2; Central European (Mac)
149 x_mac_icelandic = 10079, // x-mac-icelandic Icelandic (Mac)
150 x_mac_turkish = 10081, // x-mac-turkish Turkish (Mac)
151 x_mac_croatian = 10082, // x-mac-croatian Croatian (Mac)
152 utf32 = 12000, // utf-32 Unicode UTF-32, little endian byte order; available only to managed applications
153 utf32_be = 12001, // utf-32BE Unicode UTF-32, big endian byte order; available only to managed applications
154 x_chinese_cns = 20000, // x-Chinese_CNS CNS Taiwan; Chinese Traditional (CNS)
155 x_cp20001 = 20001, // x-cp20001 TCA Taiwan
156 x_chinese_eten = 20002, // x_Chinese-Eten Eten Taiwan; Chinese Traditional (Eten)
157 x_cp20003 = 20003, // x-cp20003 IBM5550 Taiwan
158 x_cp20004 = 20004, // x-cp20004 TeleText Taiwan
159 x_cp20005 = 20005, // x-cp20005 Wang Taiwan
160 x_ia5 = 20105, // x-IA5 IA5 (IRV International Alphabet No. 5, 7-bit); Western European (IA5)
161 x_ia5_german = 20106, // x-IA5-German IA5 German (7-bit)
162 x_ia5_swedish = 20107, // x-IA5-Swedish IA5 Swedish (7-bit)
163 x_ia5_norwegian = 20108, // x-IA5-Norwegian IA5 Norwegian (7-bit)
164 us_ascii = 20127, // us-ascii US-ASCII (7-bit)
165 x_cp20261 = 20261, // x-cp20261 T.61
166 x_cp20269 = 20269, // x-cp20269 ISO 6937 Non-Spacing Accent
167 ibm273 = 20273, // IBM273 IBM EBCDIC Germany
168 ibm277 = 20277, // IBM277 IBM EBCDIC Denmark-Norway
169 ibm278 = 20278, // IBM278 IBM EBCDIC Finland-Sweden
170 ibm280 = 20280, // IBM280 IBM EBCDIC Italy
171 ibm284 = 20284, // IBM284 IBM EBCDIC Latin America-Spain
172 ibm285 = 20285, // IBM285 IBM EBCDIC United Kingdom
173 ibm290 = 20290, // IBM290 IBM EBCDIC Japanese Katakana Extended
174 ibm297 = 20297, // IBM297 IBM EBCDIC France
175 ibm420 = 20420, // IBM420 IBM EBCDIC Arabic
176 ibm423 = 20423, // IBM423 IBM EBCDIC Greek
177 ibm424 = 20424, // IBM424 IBM EBCDIC Hebrew
178 x_ebcdic_korean_extended = 20833, // x-EBCDIC-KoreanExtended IBM EBCDIC Korean Extended
179 ibm_thai = 20838, // IBM-Thai IBM EBCDIC Thai
180 koi8_r = 20866, // koi8-r Russian (KOI8-R); Cyrillic (KOI8-R)
181 ibm871 = 20871, // IBM871 IBM EBCDIC Icelandic
182 ibm880 = 20880, // IBM880 IBM EBCDIC Cyrillic Russian
183 ibm905 = 20905, // IBM905 IBM EBCDIC Turkish
184 ibm00924 = 20924, // IBM00924 IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)
185 euc_jp_jis = 20932, // EUC-JP Japanese (JIS 0208-1990 and 0212-1990)
186 x_cp20936 = 20936, // x-cp20936 Simplified Chinese (GB2312); Chinese Simplified (GB2312-80)
187 x_cp20949 = 20949, // x-cp20949 Korean Wansung
188 cp1025 = 21025, // cp1025 IBM EBCDIC Cyrillic Serbian-Bulgarian
189 // = 21027, // (deprecated)
190 koi8_u = 21866, // koi8-u Ukrainian (KOI8-U); Cyrillic (KOI8-U)
191 iso8859_1 = 28591, // iso-8859-1 ISO 8859-1 Latin 1; Western European (ISO)
192 iso8859_2 = 28592, // iso-8859-2 ISO 8859-2 Central European; Central European (ISO)
193 iso8859_3 = 28593, // iso-8859-3 ISO 8859-3 Latin 3
194 iso8859_4 = 28594, // iso-8859-4 ISO 8859-4 Baltic
195 iso8859_5 = 28595, // iso-8859-5 ISO 8859-5 Cyrillic
196 iso8859_6 = 28596, // iso-8859-6 ISO 8859-6 Arabic
197 iso8859_7 = 28597, // iso-8859-7 ISO 8859-7 Greek
198 iso8859_8 = 28598, // iso-8859-8 ISO 8859-8 Hebrew; Hebrew (ISO-Visual)
199 iso8859_9 = 28599, // iso-8859-9 ISO 8859-9 Turkish
200 iso8859_13 = 28603, // iso-8859-13 ISO 8859-13 Estonian
201 iso8859_15 = 28605, // iso-8859-15 ISO 8859-15 Latin 9
202 x_europa = 29001, // x-Europa Europa 3
203 is8859_8_i = 38598, // iso-8859-8-i ISO 8859-8 Hebrew; Hebrew (ISO-Logical)
204 iso2022_jp = 50220, // iso-2022-jp ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS)
205 cs_iso2022_jp = 50221, // csISO2022JP ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana)
206 iso2022_jp_jis_x = 50222, // iso-2022-jp ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI)
207 iso2022_kr = 50225, // iso-2022-kr ISO 2022 Korean
208 x_cp50227 = 50227, // x-cp50227 ISO 2022 Simplified Chinese; Chinese Simplified (ISO 2022)
209 iso2022_chinesetrad = 50229, // ISO 2022 Traditional Chinese
210 ebcdic_jp_katakana_extended = 50930, // EBCDIC Japanese (Katakana) Extended
211 ebcdic_us_ca_jp = 50931, // EBCDIC US-Canada and Japanese
212 ebcdic_kr_extended = 50933, // EBCDIC Korean Extended and Korean
213 ebcdic_chinesesimp_extended = 50935, // EBCDIC Simplified Chinese Extended and Simplified Chinese
214 ebcdic_chinesesimp = 50936, // EBCDIC Simplified Chinese
215 ebcdic_us_ca_chinesetrad = 50937, // EBCDIC US-Canada and Traditional Chinese
216 ebcdic_jp_latin_extended = 50939, // EBCDIC Japanese (Latin) Extended and Japanese
217 euc_jp = 51932, // euc-jp EUC Japanese
218 euc_cn = 51936, // EUC-CN EUC Simplified Chinese; Chinese Simplified (EUC)
219 euc_kr = 51949, // euc-kr EUC Korean
220 euc_chinesetrad = 51950, // EUC Traditional Chinese
221 hz_gb2312 = 52936, // hz-gb-2312 HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)
222 gb18030 = 54936, // GB18030 Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030)
223 x_iscii_de = 57002, // x-iscii-de ISCII Devanagari
224 x_iscii_be = 57003, // x-iscii-be ISCII Bangla
225 x_iscii_ta = 57004, // x-iscii-ta ISCII Tamil
226 x_iscii_te = 57005, // x-iscii-te ISCII Telugu
227 x_iscii_as = 57006, // x-iscii-as ISCII Assamese
228 x_iscii_or = 57007, // x-iscii-or ISCII Odia
229 x_iscii_ka = 57008, // x-iscii-ka ISCII Kannada
230 x_iscii_ma = 57009, // x-iscii-ma ISCII Malayalam
231 x_iscii_gu = 57010, // x-iscii-gu ISCII Gujarati
232 x_iscii_pa = 57011, // x-iscii-pa ISCII Punjabi
233 utf7 = 65000, // utf-7 Unicode (UTF-7)
234
235 pub fn codepointAt(code_page: CodePage, index: usize, bytes: []const u8) ?Codepoint {
236 if (index >= bytes.len) return null;
237 switch (code_page) {
238 .windows1252 => {
239 // All byte values have a representation, so just convert the byte
240 return Codepoint{
241 .value = windows1252.toCodepoint(bytes[index]),
242 .byte_len = 1,
243 };
244 },
245 .utf8 => {
246 return Utf8.WellFormedDecoder.decode(bytes[index..]);
247 },
248 else => unreachable,
249 }
250 }
251
252 pub fn isSupported(code_page: CodePage) bool {
253 return switch (code_page) {
254 .windows1252, .utf8 => true,
255 else => false,
256 };
257 }
258
259 pub fn getByIdentifier(identifier: u16) !CodePage {
260 // There's probably a more efficient way to do this (e.g. ComptimeHashMap?) but
261 // this should be fine, especially since this function likely won't be called much.
262 inline for (@typeInfo(CodePage).Enum.fields) |enumField| {
263 if (identifier == enumField.value) {
264 return @field(CodePage, enumField.name);
265 }
266 }
267 return error.InvalidCodePage;
268 }
269
270 pub fn getByIdentifierEnsureSupported(identifier: u16) !CodePage {
271 const code_page = try getByIdentifier(identifier);
272 switch (isSupported(code_page)) {
273 true => return code_page,
274 false => return error.UnsupportedCodePage,
275 }
276 }
277};
278
279pub const Utf8 = struct {
280 /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section
281 /// D92 of Chapter 3 of the Unicode standard (Table 3-7 specifically).
282 ///
283 /// Note: This does not match "U+FFFD Substitution of Maximal Subparts", but instead
284 /// matches the behavior of the Windows RC compiler.
285 pub const WellFormedDecoder = struct {
286 /// Like std.unicode.utf8ByteSequenceLength, but:
287 /// - Rejects non-well-formed first bytes, i.e. C0-C1, F5-FF
288 /// - Returns an optional value instead of an error union
289 pub fn sequenceLength(first_byte: u8) ?u3 {
290 return switch (first_byte) {
291 0x00...0x7F => 1,
292 0xC2...0xDF => 2,
293 0xE0...0xEF => 3,
294 0xF0...0xF4 => 4,
295 else => null,
296 };
297 }
298
299 fn isContinuationByte(byte: u8) bool {
300 return switch (byte) {
301 0x80...0xBF => true,
302 else => false,
303 };
304 }
305
306 pub fn decode(bytes: []const u8) Codepoint {
307 std.debug.assert(bytes.len > 0);
308 const first_byte = bytes[0];
309 const expected_len = sequenceLength(first_byte) orelse {
310 return .{ .value = Codepoint.invalid, .byte_len = 1 };
311 };
312 if (expected_len == 1) return .{ .value = first_byte, .byte_len = 1 };
313
314 var value: u21 = first_byte & 0b00011111;
315 var byte_index: u8 = 1;
316 while (byte_index < @min(bytes.len, expected_len)) : (byte_index += 1) {
317 const byte = bytes[byte_index];
318 // See Table 3-7 of D92 in Chapter 3 of the Unicode Standard
319 const valid: bool = switch (byte_index) {
320 1 => switch (first_byte) {
321 0xE0 => switch (byte) {
322 0xA0...0xBF => true,
323 else => false,
324 },
325 0xED => switch (byte) {
326 0x80...0x9F => true,
327 else => false,
328 },
329 0xF0 => switch (byte) {
330 0x90...0xBF => true,
331 else => false,
332 },
333 0xF4 => switch (byte) {
334 0x80...0x8F => true,
335 else => false,
336 },
337 else => switch (byte) {
338 0x80...0xBF => true,
339 else => false,
340 },
341 },
342 else => switch (byte) {
343 0x80...0xBF => true,
344 else => false,
345 },
346 };
347
348 if (!valid) {
349 var len = byte_index;
350 // Only include the byte in the invalid sequence if it's in the range
351 // of a continuation byte. All other values should not be included in the
352 // invalid sequence.
353 if (isContinuationByte(byte)) len += 1;
354 return .{ .value = Codepoint.invalid, .byte_len = len };
355 }
356
357 value <<= 6;
358 value |= byte & 0b00111111;
359 }
360 if (byte_index != expected_len) {
361 return .{ .value = Codepoint.invalid, .byte_len = byte_index };
362 }
363 return .{ .value = value, .byte_len = expected_len };
364 }
365 };
366};
367
368test "Utf8.WellFormedDecoder" {
369 const invalid_utf8 = "\xF0\x80";
370 const decoded = Utf8.WellFormedDecoder.decode(invalid_utf8);
371 try std.testing.expectEqual(Codepoint.invalid, decoded.value);
372 try std.testing.expectEqual(@as(usize, 2), decoded.byte_len);
373}
374
375test "codepointAt invalid utf8" {
376 {
377 const invalid_utf8 = "\xf0\xf0\x80\x80\x80";
378 try std.testing.expectEqual(Codepoint{
379 .value = Codepoint.invalid,
380 .byte_len = 1,
381 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
382 try std.testing.expectEqual(Codepoint{
383 .value = Codepoint.invalid,
384 .byte_len = 2,
385 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);
386 try std.testing.expectEqual(Codepoint{
387 .value = Codepoint.invalid,
388 .byte_len = 1,
389 }, CodePage.utf8.codepointAt(3, invalid_utf8).?);
390 try std.testing.expectEqual(Codepoint{
391 .value = Codepoint.invalid,
392 .byte_len = 1,
393 }, CodePage.utf8.codepointAt(4, invalid_utf8).?);
394 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(5, invalid_utf8));
395 }
396
397 {
398 const invalid_utf8 = "\xE1\xA0\xC0";
399 try std.testing.expectEqual(Codepoint{
400 .value = Codepoint.invalid,
401 .byte_len = 2,
402 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
403 try std.testing.expectEqual(Codepoint{
404 .value = Codepoint.invalid,
405 .byte_len = 1,
406 }, CodePage.utf8.codepointAt(2, invalid_utf8).?);
407 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(3, invalid_utf8));
408 }
409
410 {
411 const invalid_utf8 = "\xD2";
412 try std.testing.expectEqual(Codepoint{
413 .value = Codepoint.invalid,
414 .byte_len = 1,
415 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
416 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, invalid_utf8));
417 }
418
419 {
420 const invalid_utf8 = "\xE1\xA0";
421 try std.testing.expectEqual(Codepoint{
422 .value = Codepoint.invalid,
423 .byte_len = 2,
424 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
425 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));
426 }
427
428 {
429 const invalid_utf8 = "\xC5\xFF";
430 try std.testing.expectEqual(Codepoint{
431 .value = Codepoint.invalid,
432 .byte_len = 1,
433 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
434 try std.testing.expectEqual(Codepoint{
435 .value = Codepoint.invalid,
436 .byte_len = 1,
437 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);
438 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));
439 }
440
441 {
442 // encoded high surrogate
443 const invalid_utf8 = "\xED\xA0\xBD";
444 try std.testing.expectEqual(Codepoint{
445 .value = Codepoint.invalid,
446 .byte_len = 2,
447 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
448 try std.testing.expectEqual(Codepoint{
449 .value = Codepoint.invalid,
450 .byte_len = 1,
451 }, CodePage.utf8.codepointAt(2, invalid_utf8).?);
452 }
453}
454
455test "codepointAt utf8 encoded" {
456 const utf8_encoded = "²";
457
458 // with code page utf8
459 try std.testing.expectEqual(Codepoint{
460 .value = '²',
461 .byte_len = 2,
462 }, CodePage.utf8.codepointAt(0, utf8_encoded).?);
463 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, utf8_encoded));
464
465 // with code page windows1252
466 try std.testing.expectEqual(Codepoint{
467 .value = '\xC2',
468 .byte_len = 1,
469 }, CodePage.windows1252.codepointAt(0, utf8_encoded).?);
470 try std.testing.expectEqual(Codepoint{
471 .value = '\xB2',
472 .byte_len = 1,
473 }, CodePage.windows1252.codepointAt(1, utf8_encoded).?);
474 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, utf8_encoded));
475}
476
477test "codepointAt windows1252 encoded" {
478 const windows1252_encoded = "\xB2";
479
480 // with code page utf8
481 try std.testing.expectEqual(Codepoint{
482 .value = Codepoint.invalid,
483 .byte_len = 1,
484 }, CodePage.utf8.codepointAt(0, windows1252_encoded).?);
485 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, windows1252_encoded));
486
487 // with code page windows1252
488 try std.testing.expectEqual(Codepoint{
489 .value = '\xB2',
490 .byte_len = 1,
491 }, CodePage.windows1252.codepointAt(0, windows1252_encoded).?);
492 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, windows1252_encoded));
493}
494
495pub const Codepoint = struct {
496 value: u21,
497 byte_len: usize,
498
499 pub const invalid: u21 = std.math.maxInt(u21);
500};
lib/compiler/resinator/comments.zig created+358
...@@ -0,0 +1,358 @@
1//! Expects to run after a C preprocessor step that preserves comments.
2//!
3//! `rc` has a peculiar quirk where something like `blah/**/blah` will be
4//! transformed into `blahblah` during parsing. However, `clang -E` will
5//! transform it into `blah blah`, so in order to match `rc`, we need
6//! to remove comments ourselves after the preprocessor runs.
7//! Note: Multiline comments that actually span more than one line do
8//! get translated to a space character by `rc`.
9//!
10//! Removing comments before lexing also allows the lexer to not have to
11//! deal with comments which would complicate its implementation (this is something
12//! of a tradeoff, as removing comments in a separate pass means that we'll
13//! need to iterate the source twice instead of once, but having to deal with
14//! comments when lexing would be a pain).
15
16const std = @import("std");
17const Allocator = std.mem.Allocator;
18const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter;
19const SourceMappings = @import("source_mapping.zig").SourceMappings;
20const LineHandler = @import("lex.zig").LineHandler;
21const formsLineEndingPair = @import("source_mapping.zig").formsLineEndingPair;
22
23/// `buf` must be at least as long as `source`
24/// In-place transformation is supported (i.e. `source` and `buf` can be the same slice)
25pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMappings) ![]u8 {
26 std.debug.assert(buf.len >= source.len);
27 var result = UncheckedSliceWriter{ .slice = buf };
28 const State = enum {
29 start,
30 forward_slash,
31 line_comment,
32 multiline_comment,
33 multiline_comment_end,
34 single_quoted,
35 single_quoted_escape,
36 double_quoted,
37 double_quoted_escape,
38 };
39 var state: State = .start;
40 var index: usize = 0;
41 var pending_start: ?usize = null;
42 var line_handler = LineHandler{ .buffer = source };
43 while (index < source.len) : (index += 1) {
44 const c = source[index];
45 // TODO: Disallow \x1A, \x00, \x7F in comments. At least \x1A and \x00 can definitely
46 // cause errors or parsing weirdness in the Win32 RC compiler. These are disallowed
47 // in the lexer, but comments are stripped before getting to the lexer.
48 switch (state) {
49 .start => switch (c) {
50 '/' => {
51 state = .forward_slash;
52 pending_start = index;
53 },
54 '\r', '\n' => {
55 _ = line_handler.incrementLineNumber(index);
56 result.write(c);
57 },
58 else => {
59 switch (c) {
60 '"' => state = .double_quoted,
61 '\'' => state = .single_quoted,
62 else => {},
63 }
64 result.write(c);
65 },
66 },
67 .forward_slash => switch (c) {
68 '/' => state = .line_comment,
69 '*' => {
70 state = .multiline_comment;
71 },
72 else => {
73 _ = line_handler.maybeIncrementLineNumber(index);
74 result.writeSlice(source[pending_start.? .. index + 1]);
75 pending_start = null;
76 state = .start;
77 },
78 },
79 .line_comment => switch (c) {
80 '\r', '\n' => {
81 _ = line_handler.incrementLineNumber(index);
82 result.write(c);
83 state = .start;
84 },
85 else => {},
86 },
87 .multiline_comment => switch (c) {
88 '\r' => try 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 try handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings);
99 // We only want to treat this as a newline if it's part of a CRLF pair. If it's
100 // not, then we still want to stay in .multiline_comment_end, so that e.g. `*<\r>/` still
101 // functions as a `*/` comment ending. Kinda crazy, but that's how the Win32 implementation works.
102 if (formsLineEndingPair(source, '\r', index + 1)) {
103 state = .multiline_comment;
104 }
105 },
106 '\n' => {
107 _ = line_handler.incrementLineNumber(index);
108 result.write(c);
109 state = .multiline_comment;
110 },
111 '/' => {
112 state = .start;
113 },
114 else => {
115 state = .multiline_comment;
116 },
117 },
118 .single_quoted => switch (c) {
119 '\r', '\n' => {
120 _ = line_handler.incrementLineNumber(index);
121 state = .start;
122 result.write(c);
123 },
124 '\\' => {
125 state = .single_quoted_escape;
126 result.write(c);
127 },
128 '\'' => {
129 state = .start;
130 result.write(c);
131 },
132 else => {
133 result.write(c);
134 },
135 },
136 .single_quoted_escape => switch (c) {
137 '\r', '\n' => {
138 _ = line_handler.incrementLineNumber(index);
139 state = .start;
140 result.write(c);
141 },
142 else => {
143 state = .single_quoted;
144 result.write(c);
145 },
146 },
147 .double_quoted => switch (c) {
148 '\r', '\n' => {
149 _ = line_handler.incrementLineNumber(index);
150 state = .start;
151 result.write(c);
152 },
153 '\\' => {
154 state = .double_quoted_escape;
155 result.write(c);
156 },
157 '"' => {
158 state = .start;
159 result.write(c);
160 },
161 else => {
162 result.write(c);
163 },
164 },
165 .double_quoted_escape => switch (c) {
166 '\r', '\n' => {
167 _ = line_handler.incrementLineNumber(index);
168 state = .start;
169 result.write(c);
170 },
171 else => {
172 state = .double_quoted;
173 result.write(c);
174 },
175 },
176 }
177 }
178 return result.getWritten();
179}
180
181inline fn handleMultilineCarriageReturn(
182 source: []const u8,
183 line_handler: *LineHandler,
184 index: usize,
185 result: *UncheckedSliceWriter,
186 source_mappings: ?*SourceMappings,
187) !void {
188 // This is a dumb way to go about this, but basically we want to determine
189 // if this is part of a distinct CRLF or LFCR pair. This function call will detect
190 // LFCR pairs correctly since the function we're in will only be called on CR,
191 // but will not detect CRLF pairs since it only looks at the line ending before the
192 // CR. So, we do a second (forward) check if the first fails to detect CRLF that is
193 // not part of another pair.
194 const is_lfcr_pair = line_handler.currentIndexFormsLineEndingPair(index);
195 const is_crlf_pair = !is_lfcr_pair and formsLineEndingPair(source, '\r', index + 1);
196 // Note: Bare \r within a multiline comment should *not* be treated as a line ending for the
197 // purposes of removing comments, but *should* be treated as a line ending for the
198 // purposes of line counting/source mapping
199 _ = line_handler.incrementLineNumber(index);
200 // So only write the \r if it's part of a CRLF/LFCR pair
201 if (is_lfcr_pair or is_crlf_pair) {
202 result.write('\r');
203 }
204 // And otherwise, we want to collapse the source mapping so that we can still know which
205 // line came from where.
206 else {
207 // Because the line gets collapsed, we need to decrement line number so that
208 // the next collapse acts on the first of the collapsed line numbers
209 line_handler.line_number -= 1;
210 if (source_mappings) |mappings| {
211 try mappings.collapse(line_handler.line_number, 1);
212 }
213 }
214}
215
216pub fn removeCommentsAlloc(allocator: Allocator, source: []const u8, source_mappings: ?*SourceMappings) ![]u8 {
217 const buf = try allocator.alloc(u8, source.len);
218 errdefer allocator.free(buf);
219 const result = try removeComments(source, buf, source_mappings);
220 return allocator.realloc(buf, result.len);
221}
222
223fn testRemoveComments(expected: []const u8, source: []const u8) !void {
224 const result = try removeCommentsAlloc(std.testing.allocator, source, null);
225 defer std.testing.allocator.free(result);
226
227 try std.testing.expectEqualStrings(expected, result);
228}
229
230test "basic" {
231 try testRemoveComments("", "// comment");
232 try testRemoveComments("", "/* comment */");
233}
234
235test "mixed" {
236 try testRemoveComments("hello", "hello// comment");
237 try testRemoveComments("hello", "hel/* comment */lo");
238}
239
240test "within a string" {
241 // escaped " is \"
242 try testRemoveComments(
243 \\blah"//som\"/*ething*/"BLAH
244 ,
245 \\blah"//som\"/*ething*/"BLAH
246 );
247}
248
249test "line comments retain newlines" {
250 try testRemoveComments(
251 \\
252 \\
253 \\
254 ,
255 \\// comment
256 \\// comment
257 \\// comment
258 );
259
260 try testRemoveComments("\r\n", "//comment\r\n");
261}
262
263test "unfinished multiline comment" {
264 try testRemoveComments(
265 \\unfinished
266 \\
267 ,
268 \\unfinished/*
269 \\
270 );
271}
272
273test "crazy" {
274 try testRemoveComments(
275 \\blah"/*som*/\""BLAH
276 ,
277 \\blah"/*som*/\""/*ething*/BLAH
278 );
279
280 try testRemoveComments(
281 \\blah"/*som*/"BLAH RCDATA "BEGIN END
282 \\
283 \\
284 \\hello
285 \\"
286 ,
287 \\blah"/*som*/"/*ething*/BLAH RCDATA "BEGIN END
288 \\// comment
289 \\//"blah blah" RCDATA {}
290 \\hello
291 \\"
292 );
293}
294
295test "multiline comment with newlines" {
296 // bare \r is not treated as a newline
297 try testRemoveComments("blahblah", "blah/*some\rthing*/blah");
298
299 try testRemoveComments(
300 \\blah
301 \\blah
302 ,
303 \\blah/*some
304 \\thing*/blah
305 );
306 try testRemoveComments(
307 "blah\r\nblah",
308 "blah/*some\r\nthing*/blah",
309 );
310
311 // handle *<not /> correctly
312 try testRemoveComments(
313 \\blah
314 \\
315 \\
316 ,
317 \\blah/*some
318 \\thing*
319 \\/bl*ah*/
320 );
321}
322
323test "comments appended to a line" {
324 try testRemoveComments(
325 \\blah
326 \\blah
327 ,
328 \\blah // line comment
329 \\blah
330 );
331 try testRemoveComments(
332 "blah \r\nblah",
333 "blah // line comment\r\nblah",
334 );
335}
336
337test "remove comments with mappings" {
338 const allocator = std.testing.allocator;
339 var mut_source = "blah/*\rcommented line*\r/blah".*;
340 var mappings = SourceMappings{};
341 _ = try mappings.files.put(allocator, "test.rc");
342 try mappings.set(1, 1, 0);
343 try mappings.set(2, 2, 0);
344 try mappings.set(3, 3, 0);
345 defer mappings.deinit(allocator);
346
347 const result = try removeComments(&mut_source, &mut_source, &mappings);
348
349 try std.testing.expectEqualStrings("blahblah", result);
350 try std.testing.expectEqual(@as(usize, 1), mappings.end_line);
351 try std.testing.expectEqual(@as(usize, 3), mappings.getCorrespondingSpan(1).?.end_line);
352}
353
354test "in place" {
355 var mut_source = "blah /* comment */ blah".*;
356 const result = try removeComments(&mut_source, &mut_source, null);
357 try std.testing.expectEqualStrings("blah blah", result);
358}
lib/compiler/resinator/compile.zig created+3427
...@@ -0,0 +1,3427 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const Node = @import("ast.zig").Node;
5const lex = @import("lex.zig");
6const Parser = @import("parse.zig").Parser;
7const Resource = @import("rc.zig").Resource;
8const Token = @import("lex.zig").Token;
9const literals = @import("literals.zig");
10const Number = literals.Number;
11const SourceBytes = literals.SourceBytes;
12const Diagnostics = @import("errors.zig").Diagnostics;
13const ErrorDetails = @import("errors.zig").ErrorDetails;
14const MemoryFlags = @import("res.zig").MemoryFlags;
15const rc = @import("rc.zig");
16const res = @import("res.zig");
17const ico = @import("ico.zig");
18const ani = @import("ani.zig");
19const bmp = @import("bmp.zig");
20const WORD = std.os.windows.WORD;
21const DWORD = std.os.windows.DWORD;
22const utils = @import("utils.zig");
23const NameOrOrdinal = res.NameOrOrdinal;
24const CodePage = @import("code_pages.zig").CodePage;
25const CodePageLookup = @import("ast.zig").CodePageLookup;
26const SourceMappings = @import("source_mapping.zig").SourceMappings;
27const windows1252 = @import("windows1252.zig");
28const lang = @import("lang.zig");
29const code_pages = @import("code_pages.zig");
30const errors = @import("errors.zig");
31const native_endian = builtin.cpu.arch.endian();
32
33pub const CompileOptions = struct {
34 cwd: std.fs.Dir,
35 diagnostics: *Diagnostics,
36 source_mappings: ?*SourceMappings = null,
37 /// List of paths (absolute or relative to `cwd`) for every file that the resources within the .rc file depend on.
38 /// Items within the list will be allocated using the allocator of the ArrayList and must be
39 /// freed by the caller.
40 /// TODO: Maybe a dedicated struct for this purpose so that it's a bit nicer to work with.
41 dependencies_list: ?*std.ArrayList([]const u8) = null,
42 default_code_page: CodePage = .windows1252,
43 ignore_include_env_var: bool = false,
44 extra_include_paths: []const []const u8 = &.{},
45 /// This is just an API convenience to allow separately passing 'system' (i.e. those
46 /// that would normally be gotten from the INCLUDE env var) include paths. This is mostly
47 /// intended for use when setting `ignore_include_env_var = true`. When `ignore_include_env_var`
48 /// is false, `system_include_paths` will be searched before the paths in the INCLUDE env var.
49 system_include_paths: []const []const u8 = &.{},
50 default_language_id: ?u16 = null,
51 // TODO: Implement verbose output
52 verbose: bool = false,
53 null_terminate_string_table_strings: bool = false,
54 /// Note: This is a u15 to ensure that the maximum number of UTF-16 code units
55 /// plus a null-terminator can always fit into a u16.
56 max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
57 silent_duplicate_control_ids: bool = false,
58 warn_instead_of_error_on_invalid_code_page: bool = false,
59};
60
61pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, options: CompileOptions) !void {
62 var lexer = lex.Lexer.init(source, .{
63 .default_code_page = options.default_code_page,
64 .source_mappings = options.source_mappings,
65 .max_string_literal_codepoints = options.max_string_literal_codepoints,
66 });
67 var parser = Parser.init(&lexer, .{
68 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
69 });
70 var tree = try parser.parse(allocator, options.diagnostics);
71 defer tree.deinit();
72
73 var search_dirs = std.ArrayList(SearchDir).init(allocator);
74 defer {
75 for (search_dirs.items) |*search_dir| {
76 search_dir.deinit(allocator);
77 }
78 search_dirs.deinit();
79 }
80
81 if (options.source_mappings) |source_mappings| {
82 const root_path = source_mappings.files.get(source_mappings.root_filename_offset);
83 // If dirname returns null, then the root path will be the same as
84 // the cwd so we don't need to add it as a distinct search path.
85 if (std.fs.path.dirname(root_path)) |root_dir_path| {
86 var root_dir = try options.cwd.openDir(root_dir_path, .{});
87 errdefer root_dir.close();
88 try search_dirs.append(.{ .dir = root_dir, .path = try allocator.dupe(u8, root_dir_path) });
89 }
90 }
91 // Re-open the passed in cwd since we want to be able to close it (std.fs.cwd() shouldn't be closed)
92 const cwd_dir = options.cwd.openDir(".", .{}) catch |err| {
93 try options.diagnostics.append(.{
94 .err = .failed_to_open_cwd,
95 .token = .{
96 .id = .invalid,
97 .start = 0,
98 .end = 0,
99 .line_number = 1,
100 },
101 .print_source_line = false,
102 .extra = .{ .file_open_error = .{
103 .err = ErrorDetails.FileOpenError.enumFromError(err),
104 .filename_string_index = undefined,
105 } },
106 });
107 return error.CompileError;
108 };
109 try search_dirs.append(.{ .dir = cwd_dir, .path = null });
110 for (options.extra_include_paths) |extra_include_path| {
111 var dir = openSearchPathDir(options.cwd, extra_include_path) catch {
112 // TODO: maybe a warning that the search path is skipped?
113 continue;
114 };
115 errdefer dir.close();
116 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, extra_include_path) });
117 }
118 for (options.system_include_paths) |system_include_path| {
119 var dir = openSearchPathDir(options.cwd, system_include_path) catch {
120 // TODO: maybe a warning that the search path is skipped?
121 continue;
122 };
123 errdefer dir.close();
124 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, system_include_path) });
125 }
126 if (!options.ignore_include_env_var) {
127 const INCLUDE = std.process.getEnvVarOwned(allocator, "INCLUDE") catch "";
128 defer allocator.free(INCLUDE);
129
130 // The only precedence here is llvm-rc which also uses the platform-specific
131 // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
132 const delimiter = switch (builtin.os.tag) {
133 .windows => ';',
134 else => ':',
135 };
136 var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
137 while (it.next()) |search_path| {
138 var dir = openSearchPathDir(options.cwd, search_path) catch continue;
139 errdefer dir.close();
140 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, search_path) });
141 }
142 }
143
144 var arena_allocator = std.heap.ArenaAllocator.init(allocator);
145 defer arena_allocator.deinit();
146 const arena = arena_allocator.allocator();
147
148 var compiler = Compiler{
149 .source = source,
150 .arena = arena,
151 .allocator = allocator,
152 .cwd = options.cwd,
153 .diagnostics = options.diagnostics,
154 .dependencies_list = options.dependencies_list,
155 .input_code_pages = &tree.input_code_pages,
156 .output_code_pages = &tree.output_code_pages,
157 // This is only safe because we know search_dirs won't be modified past this point
158 .search_dirs = search_dirs.items,
159 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
160 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
161 };
162 if (options.default_language_id) |default_language_id| {
163 compiler.state.language = res.Language.fromInt(default_language_id);
164 }
165
166 try compiler.writeRoot(tree.root(), writer);
167}
168
169pub const Compiler = struct {
170 source: []const u8,
171 arena: Allocator,
172 allocator: Allocator,
173 cwd: std.fs.Dir,
174 state: State = .{},
175 diagnostics: *Diagnostics,
176 dependencies_list: ?*std.ArrayList([]const u8),
177 input_code_pages: *const CodePageLookup,
178 output_code_pages: *const CodePageLookup,
179 search_dirs: []SearchDir,
180 null_terminate_string_table_strings: bool,
181 silent_duplicate_control_ids: bool,
182
183 pub const State = struct {
184 icon_id: u16 = 1,
185 string_tables: StringTablesByLanguage = .{},
186 language: res.Language = .{},
187 font_dir: FontDir = .{},
188 version: u32 = 0,
189 characteristics: u32 = 0,
190 };
191
192 pub fn writeRoot(self: *Compiler, root: *Node.Root, writer: anytype) !void {
193 try writeEmptyResource(writer);
194 for (root.body) |node| {
195 try self.writeNode(node, writer);
196 }
197
198 // now write the FONTDIR (if it has anything in it)
199 try self.state.font_dir.writeResData(self, writer);
200 if (self.state.font_dir.fonts.items.len != 0) {
201 // The Win32 RC compiler may write a different FONTDIR resource than us,
202 // due to it sometimes writing a non-zero-length device name/face name
203 // whereas we *always* write them both as zero-length.
204 //
205 // In practical terms, this doesn't matter, since for various reasons the format
206 // of the FONTDIR cannot be relied on and is seemingly not actually used by anything
207 // anymore. We still want to emit some sort of diagnostic for the purposes of being able
208 // to know that our .RES is intentionally not meant to be byte-for-byte identical with
209 // the rc.exe output.
210 //
211 // By using the hint type here, we allow this diagnostic to be detected in code,
212 // but it will not be printed since the end-user doesn't need to care.
213 try self.addErrorDetails(.{
214 .err = .result_contains_fontdir,
215 .type = .hint,
216 .token = undefined,
217 });
218 }
219 // once we've written every else out, we can write out the finalized STRINGTABLE resources
220 var string_tables_it = self.state.string_tables.tables.iterator();
221 while (string_tables_it.next()) |string_table_entry| {
222 var string_table_it = string_table_entry.value_ptr.blocks.iterator();
223 while (string_table_it.next()) |entry| {
224 try entry.value_ptr.writeResData(self, string_table_entry.key_ptr.*, entry.key_ptr.*, writer);
225 }
226 }
227 }
228
229 pub fn writeNode(self: *Compiler, node: *Node, writer: anytype) !void {
230 switch (node.id) {
231 .root => unreachable, // writeRoot should be called directly instead
232 .resource_external => try self.writeResourceExternal(@fieldParentPtr(Node.ResourceExternal, "base", node), writer),
233 .resource_raw_data => try self.writeResourceRawData(@fieldParentPtr(Node.ResourceRawData, "base", node), writer),
234 .literal => unreachable, // this is context dependent and should be handled by its parent
235 .binary_expression => unreachable,
236 .grouped_expression => unreachable,
237 .not_expression => unreachable,
238 .invalid => {}, // no-op, currently only used for dangling literals at EOF
239 .accelerators => try self.writeAccelerators(@fieldParentPtr(Node.Accelerators, "base", node), writer),
240 .accelerator => unreachable, // handled by writeAccelerators
241 .dialog => try self.writeDialog(@fieldParentPtr(Node.Dialog, "base", node), writer),
242 .control_statement => unreachable,
243 .toolbar => try self.writeToolbar(@fieldParentPtr(Node.Toolbar, "base", node), writer),
244 .menu => try self.writeMenu(@fieldParentPtr(Node.Menu, "base", node), writer),
245 .menu_item => unreachable,
246 .menu_item_separator => unreachable,
247 .menu_item_ex => unreachable,
248 .popup => unreachable,
249 .popup_ex => unreachable,
250 .version_info => try self.writeVersionInfo(@fieldParentPtr(Node.VersionInfo, "base", node), writer),
251 .version_statement => unreachable,
252 .block => unreachable,
253 .block_value => unreachable,
254 .block_value_value => unreachable,
255 .string_table => try self.writeStringTable(@fieldParentPtr(Node.StringTable, "base", node)),
256 .string_table_string => unreachable, // handled by writeStringTable
257 .language_statement => self.writeLanguageStatement(@fieldParentPtr(Node.LanguageStatement, "base", node)),
258 .font_statement => unreachable,
259 .simple_statement => self.writeTopLevelSimpleStatement(@fieldParentPtr(Node.SimpleStatement, "base", node)),
260 }
261 }
262
263 /// Returns the filename encoded as UTF-8 (allocated by self.allocator)
264 pub fn evaluateFilenameExpression(self: *Compiler, expression_node: *Node) ![]u8 {
265 switch (expression_node.id) {
266 .literal => {
267 const literal_node = expression_node.cast(.literal).?;
268 switch (literal_node.token.id) {
269 .literal, .number => {
270 const slice = literal_node.token.slice(self.source);
271 const code_page = self.input_code_pages.getForToken(literal_node.token);
272 var buf = try std.ArrayList(u8).initCapacity(self.allocator, slice.len);
273 errdefer buf.deinit();
274
275 var index: usize = 0;
276 while (code_page.codepointAt(index, slice)) |codepoint| : (index += codepoint.byte_len) {
277 const c = codepoint.value;
278 if (c == code_pages.Codepoint.invalid) {
279 try buf.appendSlice("�");
280 } else {
281 // Anything that is not returned as an invalid codepoint must be encodable as UTF-8.
282 const utf8_len = std.unicode.utf8CodepointSequenceLength(c) catch unreachable;
283 try buf.ensureUnusedCapacity(utf8_len);
284 _ = std.unicode.utf8Encode(c, buf.unusedCapacitySlice()) catch unreachable;
285 buf.items.len += utf8_len;
286 }
287 }
288
289 return buf.toOwnedSlice();
290 },
291 .quoted_ascii_string, .quoted_wide_string => {
292 const slice = literal_node.token.slice(self.source);
293 const column = literal_node.token.calculateColumn(self.source, 8, null);
294 const bytes = SourceBytes{ .slice = slice, .code_page = self.input_code_pages.getForToken(literal_node.token) };
295
296 var buf = std.ArrayList(u8).init(self.allocator);
297 errdefer buf.deinit();
298
299 // Filenames are sort-of parsed as if they were wide strings, but the max escape width of
300 // hex/octal escapes is still determined by the L prefix. Since we want to end up with
301 // UTF-8, we can parse either string type directly to UTF-8.
302 var parser = literals.IterativeStringParser.init(bytes, .{
303 .start_column = column,
304 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
305 });
306
307 while (try parser.nextUnchecked()) |parsed| {
308 const c = parsed.codepoint;
309 if (c == code_pages.Codepoint.invalid) {
310 try buf.appendSlice("�");
311 } else {
312 var codepoint_buf: [4]u8 = undefined;
313 // If the codepoint cannot be encoded, we fall back to �
314 if (std.unicode.utf8Encode(c, &codepoint_buf)) |len| {
315 try buf.appendSlice(codepoint_buf[0..len]);
316 } else |_| {
317 try buf.appendSlice("�");
318 }
319 }
320 }
321
322 return buf.toOwnedSlice();
323 },
324 else => unreachable, // no other token types should be in a filename literal node
325 }
326 },
327 .binary_expression => {
328 const binary_expression_node = expression_node.cast(.binary_expression).?;
329 return self.evaluateFilenameExpression(binary_expression_node.right);
330 },
331 .grouped_expression => {
332 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
333 return self.evaluateFilenameExpression(grouped_expression_node.expression);
334 },
335 else => unreachable,
336 }
337 }
338
339 /// https://learn.microsoft.com/en-us/windows/win32/menurc/searching-for-files
340 ///
341 /// Searches, in this order:
342 /// Directory of the 'root' .rc file (if different from CWD)
343 /// CWD
344 /// extra_include_paths (resolved relative to CWD)
345 /// system_include_paths (resolve relative to CWD)
346 /// INCLUDE environment var paths (only if ignore_include_env_var is false; resolved relative to CWD)
347 ///
348 /// Note: The CWD being searched *in addition to* the directory of the 'root' .rc file
349 /// is also how the Win32 RC compiler preprocessor searches for includes, but that
350 /// differs from how the clang preprocessor searches for includes.
351 ///
352 /// Note: This will always return the first matching file that can be opened.
353 /// This matches the Win32 RC compiler, which will fail with an error if the first
354 /// matching file is invalid. That is, it does not do the `cmd` PATH searching
355 /// thing of continuing to look for matching files until it finds a valid
356 /// one if a matching file is invalid.
357 fn searchForFile(self: *Compiler, path: []const u8) !std.fs.File {
358 // If the path is absolute, then it is not resolved relative to any search
359 // paths, so there's no point in checking them.
360 //
361 // This behavior was determined/confirmed with the following test:
362 // - A `test.rc` file with the contents `1 RCDATA "/test.bin"`
363 // - A `test.bin` file at `C:\test.bin`
364 // - A `test.bin` file at `inc\test.bin` relative to the .rc file
365 // - Invoking `rc` with `rc /i inc test.rc`
366 //
367 // This results in a .res file with the contents of `C:\test.bin`, not
368 // the contents of `inc\test.bin`. Further, if `C:\test.bin` is deleted,
369 // then it start failing to find `/test.bin`, meaning that it does not resolve
370 // `/test.bin` relative to include paths and instead only treats it as
371 // an absolute path.
372 if (std.fs.path.isAbsolute(path)) {
373 const file = try utils.openFileNotDir(std.fs.cwd(), path, .{});
374 errdefer file.close();
375
376 if (self.dependencies_list) |dependencies_list| {
377 const duped_path = try dependencies_list.allocator.dupe(u8, path);
378 errdefer dependencies_list.allocator.free(duped_path);
379 try dependencies_list.append(duped_path);
380 }
381 }
382
383 var first_error: ?std.fs.File.OpenError = null;
384 for (self.search_dirs) |search_dir| {
385 if (utils.openFileNotDir(search_dir.dir, path, .{})) |file| {
386 errdefer file.close();
387
388 if (self.dependencies_list) |dependencies_list| {
389 const searched_file_path = try std.fs.path.join(dependencies_list.allocator, &.{
390 search_dir.path orelse "", path,
391 });
392 errdefer dependencies_list.allocator.free(searched_file_path);
393 try dependencies_list.append(searched_file_path);
394 }
395
396 return file;
397 } else |err| if (first_error == null) {
398 first_error = err;
399 }
400 }
401 return first_error orelse error.FileNotFound;
402 }
403
404 pub fn parseDlgIncludeString(self: *Compiler, token: Token) ![]u8 {
405 // For the purposes of parsing, we want to strip the L prefix
406 // if it exists since we want escaped integers to be limited to
407 // their ascii string range.
408 //
409 // We keep track of whether or not there was an L prefix, though,
410 // since there's more weirdness to come.
411 var bytes = self.sourceBytesForToken(token);
412 var was_wide_string = false;
413 if (bytes.slice[0] == 'L' or bytes.slice[0] == 'l') {
414 was_wide_string = true;
415 bytes.slice = bytes.slice[1..];
416 }
417
418 var buf = try std.ArrayList(u8).initCapacity(self.allocator, bytes.slice.len);
419 errdefer buf.deinit();
420
421 var iterative_parser = literals.IterativeStringParser.init(bytes, .{
422 .start_column = token.calculateColumn(self.source, 8, null),
423 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
424 });
425
426 // No real idea what's going on here, but this matches the rc.exe behavior
427 while (try iterative_parser.next()) |parsed| {
428 const c = parsed.codepoint;
429 switch (was_wide_string) {
430 true => {
431 switch (c) {
432 0...0x7F, 0xA0...0xFF => try buf.append(@intCast(c)),
433 0x80...0x9F => {
434 if (windows1252.bestFitFromCodepoint(c)) |_| {
435 try buf.append(@intCast(c));
436 } else {
437 try buf.append('?');
438 }
439 },
440 else => {
441 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
442 try buf.append(best_fit);
443 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
444 try buf.append('?');
445 } else {
446 try buf.appendSlice("??");
447 }
448 },
449 }
450 },
451 false => {
452 if (parsed.from_escaped_integer) {
453 try buf.append(@truncate(c));
454 } else {
455 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
456 try buf.append(best_fit);
457 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
458 try buf.append('?');
459 } else {
460 try buf.appendSlice("??");
461 }
462 }
463 },
464 }
465 }
466
467 return buf.toOwnedSlice();
468 }
469
470 pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: anytype) !void {
471 // Init header with data size zero for now, will need to fill it in later
472 var header = try self.resourceHeader(node.id, node.type, .{});
473 defer header.deinit(self.allocator);
474
475 const maybe_predefined_type = header.predefinedResourceType();
476
477 // DLGINCLUDE has special handling that doesn't actually need the file to exist
478 if (maybe_predefined_type != null and maybe_predefined_type.? == .DLGINCLUDE) {
479 const filename_token = node.filename.cast(.literal).?.token;
480 const parsed_filename = try self.parseDlgIncludeString(filename_token);
481 defer self.allocator.free(parsed_filename);
482
483 // NUL within the parsed string acts as a terminator
484 const parsed_filename_terminated = std.mem.sliceTo(parsed_filename, 0);
485
486 header.applyMemoryFlags(node.common_resource_attributes, self.source);
487 header.data_size = @intCast(parsed_filename_terminated.len + 1);
488 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
489 try writer.writeAll(parsed_filename_terminated);
490 try writer.writeByte(0);
491 try writeDataPadding(writer, header.data_size);
492 return;
493 }
494
495 const filename_utf8 = try self.evaluateFilenameExpression(node.filename);
496 defer self.allocator.free(filename_utf8);
497
498 // TODO: More robust checking of the validity of the filename.
499 // This currently only checks for NUL bytes, but it should probably also check for
500 // platform-specific invalid characters like '*', '?', '"', '<', '>', '|' (Windows)
501 // Related: https://github.com/ziglang/zig/pull/14533#issuecomment-1416888193
502 if (std.mem.indexOfScalar(u8, filename_utf8, 0) != null) {
503 return self.addErrorDetailsAndFail(.{
504 .err = .invalid_filename,
505 .token = node.filename.getFirstToken(),
506 .token_span_end = node.filename.getLastToken(),
507 .extra = .{ .number = 0 },
508 });
509 }
510
511 // Allow plain number literals, but complex number expressions are evaluated strangely
512 // and almost certainly lead to things not intended by the user (e.g. '(1+-1)' evaluates
513 // to the filename '-1'), so error if the filename node is a grouped/binary expression.
514 // Note: This is done here instead of during parsing so that we can easily include
515 // the evaluated filename as part of the error messages.
516 if (node.filename.id != .literal) {
517 const filename_string_index = try self.diagnostics.putString(filename_utf8);
518 try self.addErrorDetails(.{
519 .err = .number_expression_as_filename,
520 .token = node.filename.getFirstToken(),
521 .token_span_end = node.filename.getLastToken(),
522 .extra = .{ .number = filename_string_index },
523 });
524 return self.addErrorDetailsAndFail(.{
525 .err = .number_expression_as_filename,
526 .type = .note,
527 .token = node.filename.getFirstToken(),
528 .token_span_end = node.filename.getLastToken(),
529 .print_source_line = false,
530 .extra = .{ .number = filename_string_index },
531 });
532 }
533 // From here on out, we know that the filename must be comprised of a single token,
534 // so get it here to simplify future usage.
535 const filename_token = node.filename.getFirstToken();
536
537 const file = self.searchForFile(filename_utf8) catch |err| switch (err) {
538 error.OutOfMemory => |e| return e,
539 else => |e| {
540 const filename_string_index = try self.diagnostics.putString(filename_utf8);
541 return self.addErrorDetailsAndFail(.{
542 .err = .file_open_error,
543 .token = filename_token,
544 .extra = .{ .file_open_error = .{
545 .err = ErrorDetails.FileOpenError.enumFromError(e),
546 .filename_string_index = filename_string_index,
547 } },
548 });
549 },
550 };
551 defer file.close();
552
553 if (maybe_predefined_type) |predefined_type| {
554 switch (predefined_type) {
555 .GROUP_ICON, .GROUP_CURSOR => {
556 // Check for animated icon first
557 if (ani.isAnimatedIcon(file.reader())) {
558 // Animated icons are just put into the resource unmodified,
559 // and the resource type changes to ANIICON/ANICURSOR
560
561 const new_predefined_type: res.RT = switch (predefined_type) {
562 .GROUP_ICON => .ANIICON,
563 .GROUP_CURSOR => .ANICURSOR,
564 else => unreachable,
565 };
566 header.type_value.ordinal = @intFromEnum(new_predefined_type);
567 header.memory_flags = MemoryFlags.defaults(new_predefined_type);
568 header.applyMemoryFlags(node.common_resource_attributes, self.source);
569 header.data_size = @intCast(try file.getEndPos());
570
571 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
572 try file.seekTo(0);
573 try writeResourceData(writer, file.reader(), header.data_size);
574 return;
575 }
576
577 // isAnimatedIcon moved the file cursor so reset to the start
578 try file.seekTo(0);
579
580 const icon_dir = ico.read(self.allocator, file.reader(), try file.getEndPos()) catch |err| switch (err) {
581 error.OutOfMemory => |e| return e,
582 else => |e| {
583 return self.iconReadError(
584 e,
585 filename_utf8,
586 filename_token,
587 predefined_type,
588 );
589 },
590 };
591 defer icon_dir.deinit();
592
593 // This limit is inherent to the ico format since number of entries is a u16 field.
594 std.debug.assert(icon_dir.entries.len <= std.math.maxInt(u16));
595
596 // Note: The Win32 RC compiler will compile the resource as whatever type is
597 // in the icon_dir regardless of the type of resource specified in the .rc.
598 // This leads to unusable .res files when the types mismatch, so
599 // we error instead.
600 const res_types_match = switch (predefined_type) {
601 .GROUP_ICON => icon_dir.image_type == .icon,
602 .GROUP_CURSOR => icon_dir.image_type == .cursor,
603 else => unreachable,
604 };
605 if (!res_types_match) {
606 return self.addErrorDetailsAndFail(.{
607 .err = .icon_dir_and_resource_type_mismatch,
608 .token = filename_token,
609 .extra = .{ .resource = switch (predefined_type) {
610 .GROUP_ICON => .icon,
611 .GROUP_CURSOR => .cursor,
612 else => unreachable,
613 } },
614 });
615 }
616
617 // Memory flags affect the RT_ICON and the RT_GROUP_ICON differently
618 var icon_memory_flags = MemoryFlags.defaults(res.RT.ICON);
619 applyToMemoryFlags(&icon_memory_flags, node.common_resource_attributes, self.source);
620 applyToGroupMemoryFlags(&header.memory_flags, node.common_resource_attributes, self.source);
621
622 const first_icon_id = self.state.icon_id;
623 const entry_type = if (predefined_type == .GROUP_ICON) @intFromEnum(res.RT.ICON) else @intFromEnum(res.RT.CURSOR);
624 for (icon_dir.entries, 0..) |*entry, entry_i_usize| {
625 // We know that the entry index must fit within a u16, so
626 // cast it here to simplify usage sites.
627 const entry_i: u16 = @intCast(entry_i_usize);
628 var full_data_size = entry.data_size_in_bytes;
629 if (icon_dir.image_type == .cursor) {
630 full_data_size = std.math.add(u32, full_data_size, 4) catch {
631 return self.addErrorDetailsAndFail(.{
632 .err = .resource_data_size_exceeds_max,
633 .token = node.id,
634 });
635 };
636 }
637
638 const image_header = ResourceHeader{
639 .type_value = .{ .ordinal = entry_type },
640 .name_value = .{ .ordinal = self.state.icon_id },
641 .data_size = full_data_size,
642 .memory_flags = icon_memory_flags,
643 .language = self.state.language,
644 .version = self.state.version,
645 .characteristics = self.state.characteristics,
646 };
647 try image_header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
648
649 // From https://learn.microsoft.com/en-us/windows/win32/menurc/localheader:
650 // > The LOCALHEADER structure is the first data written to the RT_CURSOR
651 // > resource if a RESDIR structure contains information about a cursor.
652 // where LOCALHEADER is `struct { WORD xHotSpot; WORD yHotSpot; }`
653 if (icon_dir.image_type == .cursor) {
654 try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_x, .little);
655 try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_y, .little);
656 }
657
658 try file.seekTo(entry.data_offset_from_start_of_file);
659 var header_bytes = file.reader().readBytesNoEof(16) catch {
660 return self.iconReadError(
661 error.UnexpectedEOF,
662 filename_utf8,
663 filename_token,
664 predefined_type,
665 );
666 };
667
668 const image_format = ico.ImageFormat.detect(&header_bytes);
669 if (!image_format.validate(&header_bytes)) {
670 return self.iconReadError(
671 error.InvalidHeader,
672 filename_utf8,
673 filename_token,
674 predefined_type,
675 );
676 }
677 switch (image_format) {
678 .riff => switch (icon_dir.image_type) {
679 .icon => {
680 // The Win32 RC compiler treats this as an error, but icon dirs
681 // with RIFF encoded icons within them work ~okay (they work
682 // in some places but not others, they may not animate, etc) if they are
683 // allowed to be compiled.
684 try self.addErrorDetails(.{
685 .err = .rc_would_error_on_icon_dir,
686 .type = .warning,
687 .token = filename_token,
688 .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
689 });
690 try self.addErrorDetails(.{
691 .err = .rc_would_error_on_icon_dir,
692 .type = .note,
693 .print_source_line = false,
694 .token = filename_token,
695 .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
696 });
697 },
698 .cursor => {
699 // The Win32 RC compiler errors in this case too, but we only error
700 // here because the cursor would fail to be loaded at runtime if we
701 // compiled it.
702 return self.addErrorDetailsAndFail(.{
703 .err = .format_not_supported_in_icon_dir,
704 .token = filename_token,
705 .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .riff, .index = entry_i } },
706 });
707 },
708 },
709 .png => switch (icon_dir.image_type) {
710 .icon => {
711 // PNG always seems to have 1 for color planes no matter what
712 entry.type_specific_data.icon.color_planes = 1;
713 // These seem to be the only values of num_colors that
714 // get treated specially
715 entry.type_specific_data.icon.bits_per_pixel = switch (entry.num_colors) {
716 2 => 1,
717 8 => 3,
718 16 => 4,
719 else => entry.type_specific_data.icon.bits_per_pixel,
720 };
721 },
722 .cursor => {
723 // The Win32 RC compiler treats this as an error, but cursor dirs
724 // with PNG encoded icons within them work fine if they are
725 // allowed to be compiled.
726 try self.addErrorDetails(.{
727 .err = .rc_would_error_on_icon_dir,
728 .type = .warning,
729 .token = filename_token,
730 .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .png, .index = entry_i } },
731 });
732 },
733 },
734 .dib => {
735 const bitmap_header: *ico.BitmapHeader = @ptrCast(@alignCast(&header_bytes));
736 if (native_endian == .big) {
737 std.mem.byteSwapAllFields(ico.BitmapHeader, bitmap_header);
738 }
739 const bitmap_version = ico.BitmapHeader.Version.get(bitmap_header.bcSize);
740
741 // The Win32 RC compiler only allows headers with
742 // `bcSize == sizeof(BITMAPINFOHEADER)`, but it seems unlikely
743 // that there's a good reason for that outside of too-old
744 // bitmap headers.
745 // TODO: Need to test V4 and V5 bitmaps to check they actually work
746 if (bitmap_version == .@"win2.0") {
747 return self.addErrorDetailsAndFail(.{
748 .err = .rc_would_error_on_bitmap_version,
749 .token = filename_token,
750 .extra = .{ .icon_dir = .{
751 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
752 .icon_format = image_format,
753 .index = entry_i,
754 .bitmap_version = bitmap_version,
755 } },
756 });
757 } else if (bitmap_version != .@"nt3.1") {
758 try self.addErrorDetails(.{
759 .err = .rc_would_error_on_bitmap_version,
760 .type = .warning,
761 .token = filename_token,
762 .extra = .{ .icon_dir = .{
763 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
764 .icon_format = image_format,
765 .index = entry_i,
766 .bitmap_version = bitmap_version,
767 } },
768 });
769 }
770
771 switch (icon_dir.image_type) {
772 .icon => {
773 // The values in the icon's BITMAPINFOHEADER always take precedence over
774 // the values in the IconDir, but not in the LOCALHEADER (see above).
775 entry.type_specific_data.icon.color_planes = bitmap_header.bcPlanes;
776 entry.type_specific_data.icon.bits_per_pixel = bitmap_header.bcBitCount;
777 },
778 .cursor => {
779 // Only cursors get the width/height from BITMAPINFOHEADER (icons don't)
780 entry.width = @intCast(bitmap_header.bcWidth);
781 entry.height = @intCast(bitmap_header.bcHeight);
782 entry.type_specific_data.cursor.hotspot_x = bitmap_header.bcPlanes;
783 entry.type_specific_data.cursor.hotspot_y = bitmap_header.bcBitCount;
784 },
785 }
786 },
787 }
788
789 try file.seekTo(entry.data_offset_from_start_of_file);
790 try writeResourceDataNoPadding(writer, file.reader(), entry.data_size_in_bytes);
791 try writeDataPadding(writer, full_data_size);
792
793 if (self.state.icon_id == std.math.maxInt(u16)) {
794 try self.addErrorDetails(.{
795 .err = .max_icon_ids_exhausted,
796 .print_source_line = false,
797 .token = filename_token,
798 .extra = .{ .icon_dir = .{
799 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
800 .icon_format = image_format,
801 .index = entry_i,
802 } },
803 });
804 return self.addErrorDetailsAndFail(.{
805 .err = .max_icon_ids_exhausted,
806 .type = .note,
807 .token = filename_token,
808 .extra = .{ .icon_dir = .{
809 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
810 .icon_format = image_format,
811 .index = entry_i,
812 } },
813 });
814 }
815 self.state.icon_id += 1;
816 }
817
818 header.data_size = icon_dir.getResDataSize();
819
820 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
821 try icon_dir.writeResData(writer, first_icon_id);
822 try writeDataPadding(writer, header.data_size);
823 return;
824 },
825 .RCDATA, .HTML, .MANIFEST, .MESSAGETABLE, .DLGINIT, .PLUGPLAY => {
826 header.applyMemoryFlags(node.common_resource_attributes, self.source);
827 },
828 .BITMAP => {
829 header.applyMemoryFlags(node.common_resource_attributes, self.source);
830 const file_size = try file.getEndPos();
831
832 const bitmap_info = bmp.read(file.reader(), file_size) catch |err| {
833 const filename_string_index = try self.diagnostics.putString(filename_utf8);
834 return self.addErrorDetailsAndFail(.{
835 .err = .bmp_read_error,
836 .token = filename_token,
837 .extra = .{ .bmp_read_error = .{
838 .err = ErrorDetails.BitmapReadError.enumFromError(err),
839 .filename_string_index = filename_string_index,
840 } },
841 });
842 };
843
844 if (bitmap_info.getActualPaletteByteLen() > bitmap_info.getExpectedPaletteByteLen()) {
845 const num_ignored_bytes = bitmap_info.getActualPaletteByteLen() - bitmap_info.getExpectedPaletteByteLen();
846 var number_as_bytes: [8]u8 = undefined;
847 std.mem.writeInt(u64, &number_as_bytes, num_ignored_bytes, native_endian);
848 const value_string_index = try self.diagnostics.putString(&number_as_bytes);
849 try self.addErrorDetails(.{
850 .err = .bmp_ignored_palette_bytes,
851 .type = .warning,
852 .token = filename_token,
853 .extra = .{ .number = value_string_index },
854 });
855 } else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {
856 const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();
857
858 // TODO: Make this configurable (command line option)
859 const max_missing_bytes = 4096;
860 if (num_padding_bytes > max_missing_bytes) {
861 var numbers_as_bytes: [16]u8 = undefined;
862 std.mem.writeInt(u64, numbers_as_bytes[0..8], num_padding_bytes, native_endian);
863 std.mem.writeInt(u64, numbers_as_bytes[8..16], max_missing_bytes, native_endian);
864 const values_string_index = try self.diagnostics.putString(&numbers_as_bytes);
865 try self.addErrorDetails(.{
866 .err = .bmp_too_many_missing_palette_bytes,
867 .token = filename_token,
868 .extra = .{ .number = values_string_index },
869 });
870 return self.addErrorDetailsAndFail(.{
871 .err = .bmp_too_many_missing_palette_bytes,
872 .type = .note,
873 .print_source_line = false,
874 .token = filename_token,
875 });
876 }
877
878 var number_as_bytes: [8]u8 = undefined;
879 std.mem.writeInt(u64, &number_as_bytes, num_padding_bytes, native_endian);
880 const value_string_index = try self.diagnostics.putString(&number_as_bytes);
881 try self.addErrorDetails(.{
882 .err = .bmp_missing_palette_bytes,
883 .type = .warning,
884 .token = filename_token,
885 .extra = .{ .number = value_string_index },
886 });
887 const pixel_data_len = bitmap_info.getPixelDataLen(file_size);
888 if (pixel_data_len > 0) {
889 const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);
890 std.mem.writeInt(u64, &number_as_bytes, miscompiled_bytes, native_endian);
891 const miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);
892 try self.addErrorDetails(.{
893 .err = .rc_would_miscompile_bmp_palette_padding,
894 .type = .warning,
895 .token = filename_token,
896 .extra = .{ .number = miscompiled_bytes_string_index },
897 });
898 }
899 }
900
901 // TODO: It might be possible that the calculation done in this function
902 // could underflow if the underlying file is modified while reading
903 // it, but need to think about it more to determine if that's a
904 // real possibility
905 const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));
906
907 header.data_size = bmp_bytes_to_write;
908 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
909 try file.seekTo(bmp.file_header_len);
910 const file_reader = file.reader();
911 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.dib_header_size);
912 if (bitmap_info.getBitmasksByteLen() > 0) {
913 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.getBitmasksByteLen());
914 }
915 if (bitmap_info.getExpectedPaletteByteLen() > 0) {
916 try writeResourceDataNoPadding(writer, file_reader, @intCast(bitmap_info.getActualPaletteByteLen()));
917 // We know that the number of missing palette bytes is <= 4096
918 // (see `bmp_too_many_missing_palette_bytes` error case above)
919 const padding_bytes: usize = @intCast(bitmap_info.getMissingPaletteByteLen());
920 if (padding_bytes > 0) {
921 try writer.writeByteNTimes(0, padding_bytes);
922 }
923 }
924 try file.seekTo(bitmap_info.pixel_data_offset);
925 const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);
926 try writeResourceDataNoPadding(writer, file_reader, pixel_bytes);
927 try writeDataPadding(writer, bmp_bytes_to_write);
928 return;
929 },
930 .FONT => {
931 if (self.state.font_dir.ids.get(header.name_value.ordinal) != null) {
932 // Add warning and skip this resource
933 // Note: The Win32 compiler prints this as an error but it doesn't fail the compilation
934 // and the duplicate resource is skipped.
935 try self.addErrorDetails(ErrorDetails{
936 .err = .font_id_already_defined,
937 .token = node.id,
938 .type = .warning,
939 .extra = .{ .number = header.name_value.ordinal },
940 });
941 try self.addErrorDetails(ErrorDetails{
942 .err = .font_id_already_defined,
943 .token = self.state.font_dir.ids.get(header.name_value.ordinal).?,
944 .type = .note,
945 .extra = .{ .number = header.name_value.ordinal },
946 });
947 return;
948 }
949 header.applyMemoryFlags(node.common_resource_attributes, self.source);
950 const file_size = try file.getEndPos();
951 if (file_size > std.math.maxInt(u32)) {
952 return self.addErrorDetailsAndFail(.{
953 .err = .resource_data_size_exceeds_max,
954 .token = node.id,
955 });
956 }
957
958 // We now know that the data size will fit in a u32
959 header.data_size = @intCast(file_size);
960 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
961
962 var header_slurping_reader = headerSlurpingReader(148, file.reader());
963 try writeResourceData(writer, header_slurping_reader.reader(), header.data_size);
964
965 try self.state.font_dir.add(self.arena, FontDir.Font{
966 .id = header.name_value.ordinal,
967 .header_bytes = header_slurping_reader.slurped_header,
968 }, node.id);
969 return;
970 },
971 .ACCELERATOR,
972 .ANICURSOR,
973 .ANIICON,
974 .CURSOR,
975 .DIALOG,
976 .DLGINCLUDE,
977 .FONTDIR,
978 .ICON,
979 .MENU,
980 .STRING,
981 .TOOLBAR,
982 .VERSION,
983 .VXD,
984 => unreachable,
985 _ => unreachable,
986 }
987 } else {
988 header.applyMemoryFlags(node.common_resource_attributes, self.source);
989 }
990
991 // Fallback to just writing out the entire contents of the file
992 const data_size = try file.getEndPos();
993 if (data_size > std.math.maxInt(u32)) {
994 return self.addErrorDetailsAndFail(.{
995 .err = .resource_data_size_exceeds_max,
996 .token = node.id,
997 });
998 }
999 // We now know that the data size will fit in a u32
1000 header.data_size = @intCast(data_size);
1001 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1002 try writeResourceData(writer, file.reader(), header.data_size);
1003 }
1004
1005 fn iconReadError(
1006 self: *Compiler,
1007 err: ico.ReadError,
1008 filename: []const u8,
1009 token: Token,
1010 predefined_type: res.RT,
1011 ) error{ CompileError, OutOfMemory } {
1012 const filename_string_index = try self.diagnostics.putString(filename);
1013 return self.addErrorDetailsAndFail(.{
1014 .err = .icon_read_error,
1015 .token = token,
1016 .extra = .{ .icon_read_error = .{
1017 .err = ErrorDetails.IconReadError.enumFromError(err),
1018 .icon_type = switch (predefined_type) {
1019 .GROUP_ICON => .icon,
1020 .GROUP_CURSOR => .cursor,
1021 else => unreachable,
1022 },
1023 .filename_string_index = filename_string_index,
1024 } },
1025 });
1026 }
1027
1028 pub const DataType = enum {
1029 number,
1030 ascii_string,
1031 wide_string,
1032 };
1033
1034 pub const Data = union(DataType) {
1035 number: Number,
1036 ascii_string: []const u8,
1037 wide_string: [:0]const u16,
1038
1039 pub fn deinit(self: Data, allocator: Allocator) void {
1040 switch (self) {
1041 .wide_string => |wide_string| {
1042 allocator.free(wide_string);
1043 },
1044 .ascii_string => |ascii_string| {
1045 allocator.free(ascii_string);
1046 },
1047 else => {},
1048 }
1049 }
1050
1051 pub fn write(self: Data, writer: anytype) !void {
1052 switch (self) {
1053 .number => |number| switch (number.is_long) {
1054 false => try writer.writeInt(WORD, number.asWord(), .little),
1055 true => try writer.writeInt(DWORD, number.value, .little),
1056 },
1057 .ascii_string => |ascii_string| {
1058 try writer.writeAll(ascii_string);
1059 },
1060 .wide_string => |wide_string| {
1061 try writer.writeAll(std.mem.sliceAsBytes(wide_string));
1062 },
1063 }
1064 }
1065 };
1066
1067 /// Assumes that the node is a number or number expression
1068 pub fn evaluateNumberExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) Number {
1069 switch (expression_node.id) {
1070 .literal => {
1071 const literal_node = expression_node.cast(.literal).?;
1072 std.debug.assert(literal_node.token.id == .number);
1073 const bytes = SourceBytes{
1074 .slice = literal_node.token.slice(source),
1075 .code_page = code_page_lookup.getForToken(literal_node.token),
1076 };
1077 return literals.parseNumberLiteral(bytes);
1078 },
1079 .binary_expression => {
1080 const binary_expression_node = expression_node.cast(.binary_expression).?;
1081 const lhs = evaluateNumberExpression(binary_expression_node.left, source, code_page_lookup);
1082 const rhs = evaluateNumberExpression(binary_expression_node.right, source, code_page_lookup);
1083 const operator_char = binary_expression_node.operator.slice(source)[0];
1084 return lhs.evaluateOperator(operator_char, rhs);
1085 },
1086 .grouped_expression => {
1087 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
1088 return evaluateNumberExpression(grouped_expression_node.expression, source, code_page_lookup);
1089 },
1090 else => unreachable,
1091 }
1092 }
1093
1094 const FlagsNumber = struct {
1095 value: u32,
1096 not_mask: u32 = 0xFFFFFFFF,
1097
1098 pub fn evaluateOperator(lhs: FlagsNumber, operator_char: u8, rhs: FlagsNumber) FlagsNumber {
1099 const result = switch (operator_char) {
1100 '-' => lhs.value -% rhs.value,
1101 '+' => lhs.value +% rhs.value,
1102 '|' => lhs.value | rhs.value,
1103 '&' => lhs.value & rhs.value,
1104 else => unreachable, // invalid operator, this would be a lexer/parser bug
1105 };
1106 return .{
1107 .value = result,
1108 .not_mask = lhs.not_mask & rhs.not_mask,
1109 };
1110 }
1111
1112 pub fn applyNotMask(self: FlagsNumber) u32 {
1113 return self.value & self.not_mask;
1114 }
1115 };
1116
1117 pub fn evaluateFlagsExpressionWithDefault(default: u32, expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) u32 {
1118 var context = FlagsExpressionContext{ .initial_value = default };
1119 const number = evaluateFlagsExpression(expression_node, source, code_page_lookup, &context);
1120 return number.value;
1121 }
1122
1123 pub const FlagsExpressionContext = struct {
1124 initial_value: u32 = 0,
1125 initial_value_used: bool = false,
1126 };
1127
1128 /// Assumes that the node is a number expression (which can contain not_expressions)
1129 pub fn evaluateFlagsExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup, context: *FlagsExpressionContext) FlagsNumber {
1130 switch (expression_node.id) {
1131 .literal => {
1132 const literal_node = expression_node.cast(.literal).?;
1133 std.debug.assert(literal_node.token.id == .number);
1134 const bytes = SourceBytes{
1135 .slice = literal_node.token.slice(source),
1136 .code_page = code_page_lookup.getForToken(literal_node.token),
1137 };
1138 var value = literals.parseNumberLiteral(bytes).value;
1139 if (!context.initial_value_used) {
1140 context.initial_value_used = true;
1141 value |= context.initial_value;
1142 }
1143 return .{ .value = value };
1144 },
1145 .binary_expression => {
1146 const binary_expression_node = expression_node.cast(.binary_expression).?;
1147 const lhs = evaluateFlagsExpression(binary_expression_node.left, source, code_page_lookup, context);
1148 const rhs = evaluateFlagsExpression(binary_expression_node.right, source, code_page_lookup, context);
1149 const operator_char = binary_expression_node.operator.slice(source)[0];
1150 const result = lhs.evaluateOperator(operator_char, rhs);
1151 return .{ .value = result.applyNotMask() };
1152 },
1153 .grouped_expression => {
1154 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
1155 return evaluateFlagsExpression(grouped_expression_node.expression, source, code_page_lookup, context);
1156 },
1157 .not_expression => {
1158 const not_expression = expression_node.cast(.not_expression).?;
1159 const bytes = SourceBytes{
1160 .slice = not_expression.number_token.slice(source),
1161 .code_page = code_page_lookup.getForToken(not_expression.number_token),
1162 };
1163 const not_number = literals.parseNumberLiteral(bytes);
1164 if (!context.initial_value_used) {
1165 context.initial_value_used = true;
1166 return .{ .value = context.initial_value & ~not_number.value };
1167 }
1168 return .{ .value = 0, .not_mask = ~not_number.value };
1169 },
1170 else => unreachable,
1171 }
1172 }
1173
1174 pub fn evaluateDataExpression(self: *Compiler, expression_node: *Node) !Data {
1175 switch (expression_node.id) {
1176 .literal => {
1177 const literal_node = expression_node.cast(.literal).?;
1178 switch (literal_node.token.id) {
1179 .number => {
1180 const number = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
1181 return .{ .number = number };
1182 },
1183 .quoted_ascii_string => {
1184 const column = literal_node.token.calculateColumn(self.source, 8, null);
1185 const bytes = SourceBytes{
1186 .slice = literal_node.token.slice(self.source),
1187 .code_page = self.input_code_pages.getForToken(literal_node.token),
1188 };
1189 const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{
1190 .start_column = column,
1191 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
1192 .output_code_page = self.output_code_pages.getForToken(literal_node.token),
1193 });
1194 errdefer self.allocator.free(parsed);
1195 return .{ .ascii_string = parsed };
1196 },
1197 .quoted_wide_string => {
1198 const column = literal_node.token.calculateColumn(self.source, 8, null);
1199 const bytes = SourceBytes{
1200 .slice = literal_node.token.slice(self.source),
1201 .code_page = self.input_code_pages.getForToken(literal_node.token),
1202 };
1203 const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{
1204 .start_column = column,
1205 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
1206 });
1207 errdefer self.allocator.free(parsed_string);
1208 return .{ .wide_string = parsed_string };
1209 },
1210 else => unreachable, // no other token types should be in a data literal node
1211 }
1212 },
1213 .binary_expression, .grouped_expression => {
1214 const result = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
1215 return .{ .number = result };
1216 },
1217 .not_expression => unreachable,
1218 else => unreachable,
1219 }
1220 }
1221
1222 pub fn writeResourceRawData(self: *Compiler, node: *Node.ResourceRawData, writer: anytype) !void {
1223 var data_buffer = std.ArrayList(u8).init(self.allocator);
1224 defer data_buffer.deinit();
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 for (node.raw_data) |expression| {
1231 const data = try self.evaluateDataExpression(expression);
1232 defer data.deinit(self.allocator);
1233 data.write(data_writer) catch |err| switch (err) {
1234 error.NoSpaceLeft => {
1235 return self.addErrorDetailsAndFail(.{
1236 .err = .resource_data_size_exceeds_max,
1237 .token = node.id,
1238 });
1239 },
1240 else => |e| return e,
1241 };
1242 }
1243
1244 // This intCast can't fail because the limitedWriter above guarantees that
1245 // we will never write more than maxInt(u32) bytes.
1246 const data_len: u32 = @intCast(data_buffer.items.len);
1247 try self.writeResourceHeader(writer, node.id, node.type, data_len, node.common_resource_attributes, self.state.language);
1248
1249 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1250 try writeResourceData(writer, data_fbs.reader(), data_len);
1251 }
1252
1253 pub fn writeResourceHeader(self: *Compiler, writer: anytype, id_token: Token, type_token: Token, data_size: u32, common_resource_attributes: []Token, language: res.Language) !void {
1254 var header = try self.resourceHeader(id_token, type_token, .{
1255 .language = language,
1256 .data_size = data_size,
1257 });
1258 defer header.deinit(self.allocator);
1259
1260 header.applyMemoryFlags(common_resource_attributes, self.source);
1261
1262 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = id_token });
1263 }
1264
1265 pub fn writeResourceDataNoPadding(writer: anytype, data_reader: anytype, data_size: u32) !void {
1266 var limited_reader = std.io.limitedReader(data_reader, data_size);
1267
1268 const FifoBuffer = std.fifo.LinearFifo(u8, .{ .Static = 4096 });
1269 var fifo = FifoBuffer.init();
1270 try fifo.pump(limited_reader.reader(), writer);
1271 }
1272
1273 pub fn writeResourceData(writer: anytype, data_reader: anytype, data_size: u32) !void {
1274 try writeResourceDataNoPadding(writer, data_reader, data_size);
1275 try writeDataPadding(writer, data_size);
1276 }
1277
1278 pub fn writeDataPadding(writer: anytype, data_size: u32) !void {
1279 try writer.writeByteNTimes(0, numPaddingBytesNeeded(data_size));
1280 }
1281
1282 pub fn numPaddingBytesNeeded(data_size: u32) u2 {
1283 // Result is guaranteed to be between 0 and 3.
1284 return @intCast((4 -% data_size) % 4);
1285 }
1286
1287 pub fn evaluateAcceleratorKeyExpression(self: *Compiler, node: *Node, is_virt: bool) !u16 {
1288 if (node.isNumberExpression()) {
1289 return evaluateNumberExpression(node, self.source, self.input_code_pages).asWord();
1290 } else {
1291 std.debug.assert(node.isStringLiteral());
1292 const literal = @fieldParentPtr(Node.Literal, "base", node);
1293 const bytes = SourceBytes{
1294 .slice = literal.token.slice(self.source),
1295 .code_page = self.input_code_pages.getForToken(literal.token),
1296 };
1297 const column = literal.token.calculateColumn(self.source, 8, null);
1298 return res.parseAcceleratorKeyString(bytes, is_virt, .{
1299 .start_column = column,
1300 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal.token },
1301 });
1302 }
1303 }
1304
1305 pub fn writeAccelerators(self: *Compiler, node: *Node.Accelerators, writer: anytype) !void {
1306 var data_buffer = std.ArrayList(u8).init(self.allocator);
1307 defer data_buffer.deinit();
1308
1309 // The header's data length field is a u32 so limit the resource's data size so that
1310 // we know we can always specify the real size.
1311 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1312 const data_writer = limited_writer.writer();
1313
1314 self.writeAcceleratorsData(node, data_writer) catch |err| switch (err) {
1315 error.NoSpaceLeft => {
1316 return self.addErrorDetailsAndFail(.{
1317 .err = .resource_data_size_exceeds_max,
1318 .token = node.id,
1319 });
1320 },
1321 else => |e| return e,
1322 };
1323
1324 // This intCast can't fail because the limitedWriter above guarantees that
1325 // we will never write more than maxInt(u32) bytes.
1326 const data_size: u32 = @intCast(data_buffer.items.len);
1327 var header = try self.resourceHeader(node.id, node.type, .{
1328 .data_size = data_size,
1329 });
1330 defer header.deinit(self.allocator);
1331
1332 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1333 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
1334
1335 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1336
1337 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1338 try writeResourceData(writer, data_fbs.reader(), data_size);
1339 }
1340
1341 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
1342 /// the writer within this function could return error.NoSpaceLeft
1343 pub fn writeAcceleratorsData(self: *Compiler, node: *Node.Accelerators, data_writer: anytype) !void {
1344 for (node.accelerators, 0..) |accel_node, i| {
1345 const accelerator = @fieldParentPtr(Node.Accelerator, "base", accel_node);
1346 var modifiers = res.AcceleratorModifiers{};
1347 for (accelerator.type_and_options) |type_or_option| {
1348 const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;
1349 modifiers.apply(modifier);
1350 }
1351 if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {
1352 return self.addErrorDetailsAndFail(.{
1353 .err = .accelerator_type_required,
1354 .token = accelerator.event.getFirstToken(),
1355 .token_span_end = accelerator.event.getLastToken(),
1356 });
1357 }
1358 const key = self.evaluateAcceleratorKeyExpression(accelerator.event, modifiers.isSet(.virtkey)) catch |err| switch (err) {
1359 error.OutOfMemory => |e| return e,
1360 else => |e| {
1361 return self.addErrorDetailsAndFail(.{
1362 .err = .invalid_accelerator_key,
1363 .token = accelerator.event.getFirstToken(),
1364 .token_span_end = accelerator.event.getLastToken(),
1365 .extra = .{ .accelerator_error = .{
1366 .err = ErrorDetails.AcceleratorError.enumFromError(e),
1367 } },
1368 });
1369 },
1370 };
1371 const cmd_id = evaluateNumberExpression(accelerator.idvalue, self.source, self.input_code_pages);
1372
1373 if (i == node.accelerators.len - 1) {
1374 modifiers.markLast();
1375 }
1376
1377 try data_writer.writeByte(modifiers.value);
1378 try data_writer.writeByte(0); // padding
1379 try data_writer.writeInt(u16, key, .little);
1380 try data_writer.writeInt(u16, cmd_id.asWord(), .little);
1381 try data_writer.writeInt(u16, 0, .little); // padding
1382 }
1383 }
1384
1385 const DialogOptionalStatementValues = struct {
1386 style: u32 = res.WS.SYSMENU | res.WS.BORDER | res.WS.POPUP,
1387 exstyle: u32 = 0,
1388 class: ?NameOrOrdinal = null,
1389 menu: ?NameOrOrdinal = null,
1390 font: ?FontStatementValues = null,
1391 caption: ?Token = null,
1392 };
1393
1394 pub fn writeDialog(self: *Compiler, node: *Node.Dialog, writer: anytype) !void {
1395 var data_buffer = std.ArrayList(u8).init(self.allocator);
1396 defer data_buffer.deinit();
1397 // The header's data length field is a u32 so limit the resource's data size so that
1398 // we know we can always specify the real size.
1399 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1400 const data_writer = limited_writer.writer();
1401
1402 const resource = Resource.fromString(.{
1403 .slice = node.type.slice(self.source),
1404 .code_page = self.input_code_pages.getForToken(node.type),
1405 });
1406 std.debug.assert(resource == .dialog or resource == .dialogex);
1407
1408 var optional_statement_values: DialogOptionalStatementValues = .{};
1409 defer {
1410 if (optional_statement_values.class) |class| {
1411 class.deinit(self.allocator);
1412 }
1413 if (optional_statement_values.menu) |menu| {
1414 menu.deinit(self.allocator);
1415 }
1416 }
1417 var skipped_menu_or_classes = std.ArrayList(*Node.SimpleStatement).init(self.allocator);
1418 defer skipped_menu_or_classes.deinit();
1419 var last_menu: *Node.SimpleStatement = undefined;
1420 var last_class: *Node.SimpleStatement = undefined;
1421 var last_menu_would_be_forced_ordinal = false;
1422 var last_menu_has_digit_as_first_char = false;
1423 var last_menu_did_uppercase = false;
1424 var last_class_would_be_forced_ordinal = false;
1425
1426 for (node.optional_statements) |optional_statement| {
1427 switch (optional_statement.id) {
1428 .simple_statement => {
1429 const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", optional_statement);
1430 const statement_identifier = simple_statement.identifier;
1431 const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
1432 switch (statement_type) {
1433 .style, .exstyle => {
1434 const style = evaluateFlagsExpressionWithDefault(0, simple_statement.value, self.source, self.input_code_pages);
1435 if (statement_type == .style) {
1436 optional_statement_values.style = style;
1437 } else {
1438 optional_statement_values.exstyle = style;
1439 }
1440 },
1441 .caption => {
1442 std.debug.assert(simple_statement.value.id == .literal);
1443 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1444 optional_statement_values.caption = literal_node.token;
1445 },
1446 .class => {
1447 const is_duplicate = optional_statement_values.class != null;
1448 if (is_duplicate) {
1449 try skipped_menu_or_classes.append(last_class);
1450 }
1451 const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;
1452 // In the Win32 RC compiler, if any CLASS values that are interpreted as
1453 // an ordinal exist, it affects all future CLASS statements and forces
1454 // them to be treated as an ordinal no matter what.
1455 if (forced_ordinal) {
1456 last_class_would_be_forced_ordinal = true;
1457 }
1458 // clear out the old one if it exists
1459 if (optional_statement_values.class) |prev| {
1460 prev.deinit(self.allocator);
1461 optional_statement_values.class = null;
1462 }
1463
1464 if (simple_statement.value.isNumberExpression()) {
1465 const class_ordinal = evaluateNumberExpression(simple_statement.value, self.source, self.input_code_pages);
1466 optional_statement_values.class = NameOrOrdinal{ .ordinal = class_ordinal.asWord() };
1467 } else {
1468 std.debug.assert(simple_statement.value.isStringLiteral());
1469 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1470 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
1471 optional_statement_values.class = NameOrOrdinal{ .name = parsed };
1472 }
1473
1474 last_class = simple_statement;
1475 },
1476 .menu => {
1477 const is_duplicate = optional_statement_values.menu != null;
1478 if (is_duplicate) {
1479 try skipped_menu_or_classes.append(last_menu);
1480 }
1481 const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;
1482 // In the Win32 RC compiler, if any MENU values that are interpreted as
1483 // an ordinal exist, it affects all future MENU statements and forces
1484 // them to be treated as an ordinal no matter what.
1485 if (forced_ordinal) {
1486 last_menu_would_be_forced_ordinal = true;
1487 }
1488 // clear out the old one if it exists
1489 if (optional_statement_values.menu) |prev| {
1490 prev.deinit(self.allocator);
1491 optional_statement_values.menu = null;
1492 }
1493
1494 std.debug.assert(simple_statement.value.id == .literal);
1495 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1496
1497 const token_slice = literal_node.token.slice(self.source);
1498 const bytes = SourceBytes{
1499 .slice = token_slice,
1500 .code_page = self.input_code_pages.getForToken(literal_node.token),
1501 };
1502 optional_statement_values.menu = try NameOrOrdinal.fromString(self.allocator, bytes);
1503
1504 if (optional_statement_values.menu.? == .name) {
1505 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(bytes)) |win32_rc_ordinal| {
1506 try self.addErrorDetails(.{
1507 .err = .invalid_digit_character_in_ordinal,
1508 .type = .err,
1509 .token = literal_node.token,
1510 });
1511 return self.addErrorDetailsAndFail(.{
1512 .err = .win32_non_ascii_ordinal,
1513 .type = .note,
1514 .token = literal_node.token,
1515 .print_source_line = false,
1516 .extra = .{ .number = win32_rc_ordinal.ordinal },
1517 });
1518 }
1519 }
1520
1521 // Need to keep track of some properties of the value
1522 // in order to emit the appropriate warning(s) later on.
1523 // See where the warning are emitted below (outside this loop)
1524 // for the full explanation.
1525 var did_uppercase = false;
1526 var codepoint_i: usize = 0;
1527 while (bytes.code_page.codepointAt(codepoint_i, bytes.slice)) |codepoint| : (codepoint_i += codepoint.byte_len) {
1528 const c = codepoint.value;
1529 switch (c) {
1530 'a'...'z' => {
1531 did_uppercase = true;
1532 break;
1533 },
1534 else => {},
1535 }
1536 }
1537 last_menu_did_uppercase = did_uppercase;
1538 last_menu_has_digit_as_first_char = std.ascii.isDigit(token_slice[0]);
1539 last_menu = simple_statement;
1540 },
1541 else => {},
1542 }
1543 },
1544 .font_statement => {
1545 const font = @fieldParentPtr(Node.FontStatement, "base", optional_statement);
1546 if (optional_statement_values.font != null) {
1547 optional_statement_values.font.?.node = font;
1548 } else {
1549 optional_statement_values.font = FontStatementValues{ .node = font };
1550 }
1551 if (font.weight) |weight| {
1552 const value = evaluateNumberExpression(weight, self.source, self.input_code_pages);
1553 optional_statement_values.font.?.weight = value.asWord();
1554 }
1555 if (font.italic) |italic| {
1556 const value = evaluateNumberExpression(italic, self.source, self.input_code_pages);
1557 optional_statement_values.font.?.italic = value.asWord() != 0;
1558 }
1559 },
1560 else => {},
1561 }
1562 }
1563
1564 for (skipped_menu_or_classes.items) |simple_statement| {
1565 const statement_identifier = simple_statement.identifier;
1566 const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
1567 try self.addErrorDetails(.{
1568 .err = .duplicate_menu_or_class_skipped,
1569 .type = .warning,
1570 .token = simple_statement.identifier,
1571 .token_span_start = simple_statement.base.getFirstToken(),
1572 .token_span_end = simple_statement.base.getLastToken(),
1573 .extra = .{ .menu_or_class = switch (statement_type) {
1574 .menu => .menu,
1575 .class => .class,
1576 else => unreachable,
1577 } },
1578 });
1579 }
1580 // The Win32 RC compiler miscompiles the value in the following scenario:
1581 // Multiple CLASS parameters are specified and any of them are treated as a number, then
1582 // the last CLASS is always treated as a number no matter what
1583 if (last_class_would_be_forced_ordinal and optional_statement_values.class.? == .name) {
1584 const literal_node = @fieldParentPtr(Node.Literal, "base", last_class.value);
1585 const ordinal_value = res.ForcedOrdinal.fromUtf16Le(optional_statement_values.class.?.name);
1586
1587 try self.addErrorDetails(.{
1588 .err = .rc_would_miscompile_dialog_class,
1589 .type = .warning,
1590 .token = literal_node.token,
1591 .extra = .{ .number = ordinal_value },
1592 });
1593 try self.addErrorDetails(.{
1594 .err = .rc_would_miscompile_dialog_class,
1595 .type = .note,
1596 .print_source_line = false,
1597 .token = literal_node.token,
1598 .extra = .{ .number = ordinal_value },
1599 });
1600 try self.addErrorDetails(.{
1601 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
1602 .type = .note,
1603 .print_source_line = false,
1604 .token = literal_node.token,
1605 .extra = .{ .menu_or_class = .class },
1606 });
1607 }
1608 // The Win32 RC compiler miscompiles the id in two different scenarios:
1609 // 1. The first character of the ID is a digit, in which case it is always treated as a number
1610 // no matter what (and therefore does not match how the MENU/MENUEX id is parsed)
1611 // 2. Multiple MENU parameters are specified and any of them are treated as a number, then
1612 // the last MENU is always treated as a number no matter what
1613 if ((last_menu_would_be_forced_ordinal or last_menu_has_digit_as_first_char) and optional_statement_values.menu.? == .name) {
1614 const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
1615 const token_slice = literal_node.token.slice(self.source);
1616 const bytes = SourceBytes{
1617 .slice = token_slice,
1618 .code_page = self.input_code_pages.getForToken(literal_node.token),
1619 };
1620 const ordinal_value = res.ForcedOrdinal.fromBytes(bytes);
1621
1622 try self.addErrorDetails(.{
1623 .err = .rc_would_miscompile_dialog_menu_id,
1624 .type = .warning,
1625 .token = literal_node.token,
1626 .extra = .{ .number = ordinal_value },
1627 });
1628 try self.addErrorDetails(.{
1629 .err = .rc_would_miscompile_dialog_menu_id,
1630 .type = .note,
1631 .print_source_line = false,
1632 .token = literal_node.token,
1633 .extra = .{ .number = ordinal_value },
1634 });
1635 if (last_menu_would_be_forced_ordinal) {
1636 try self.addErrorDetails(.{
1637 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
1638 .type = .note,
1639 .print_source_line = false,
1640 .token = literal_node.token,
1641 .extra = .{ .menu_or_class = .menu },
1642 });
1643 } else {
1644 try self.addErrorDetails(.{
1645 .err = .rc_would_miscompile_dialog_menu_id_starts_with_digit,
1646 .type = .note,
1647 .print_source_line = false,
1648 .token = literal_node.token,
1649 });
1650 }
1651 }
1652 // The MENU id parsing uses the exact same logic as the MENU/MENUEX resource id parsing,
1653 // which means that it will convert ASCII characters to uppercase during the 'name' parsing.
1654 // This turns out not to matter (`LoadMenu` does a case-insensitive lookup anyway),
1655 // but it still makes sense to share the uppercasing logic since the MENU parameter
1656 // here is just a reference to a MENU/MENUEX id within the .exe.
1657 // So, because this is an intentional but inconsequential-to-the-user difference
1658 // between resinator and the Win32 RC compiler, we only emit a hint instead of
1659 // a warning.
1660 if (last_menu_did_uppercase) {
1661 const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
1662 try self.addErrorDetails(.{
1663 .err = .dialog_menu_id_was_uppercased,
1664 .type = .hint,
1665 .token = literal_node.token,
1666 });
1667 }
1668
1669 const x = evaluateNumberExpression(node.x, self.source, self.input_code_pages);
1670 const y = evaluateNumberExpression(node.y, self.source, self.input_code_pages);
1671 const width = evaluateNumberExpression(node.width, self.source, self.input_code_pages);
1672 const height = evaluateNumberExpression(node.height, self.source, self.input_code_pages);
1673
1674 // FONT statement requires DS_SETFONT, and if it's not present DS_SETFRONT must be unset
1675 if (optional_statement_values.font) |_| {
1676 optional_statement_values.style |= res.DS.SETFONT;
1677 } else {
1678 optional_statement_values.style &= ~res.DS.SETFONT;
1679 }
1680 // CAPTION statement implies WS_CAPTION
1681 if (optional_statement_values.caption) |_| {
1682 optional_statement_values.style |= res.WS.CAPTION;
1683 }
1684
1685 self.writeDialogHeaderAndStrings(
1686 node,
1687 data_writer,
1688 resource,
1689 &optional_statement_values,
1690 x,
1691 y,
1692 width,
1693 height,
1694 ) catch |err| switch (err) {
1695 // Dialog header and menu/class/title strings can never exceed u32 bytes
1696 // on their own, so this error is unreachable.
1697 error.NoSpaceLeft => unreachable,
1698 else => |e| return e,
1699 };
1700
1701 var controls_by_id = std.AutoHashMap(u32, *const Node.ControlStatement).init(self.allocator);
1702 // Number of controls are guaranteed by the parser to be within maxInt(u16).
1703 try controls_by_id.ensureTotalCapacity(@as(u16, @intCast(node.controls.len)));
1704 defer controls_by_id.deinit();
1705
1706 for (node.controls) |control_node| {
1707 const control = @fieldParentPtr(Node.ControlStatement, "base", control_node);
1708
1709 self.writeDialogControl(
1710 control,
1711 data_writer,
1712 resource,
1713 // We know the data_buffer len is limited to u32 max.
1714 @intCast(data_buffer.items.len),
1715 &controls_by_id,
1716 ) catch |err| switch (err) {
1717 error.NoSpaceLeft => {
1718 try self.addErrorDetails(.{
1719 .err = .resource_data_size_exceeds_max,
1720 .token = node.id,
1721 });
1722 return self.addErrorDetailsAndFail(.{
1723 .err = .resource_data_size_exceeds_max,
1724 .type = .note,
1725 .token = control.type,
1726 });
1727 },
1728 else => |e| return e,
1729 };
1730 }
1731
1732 // We know the data_buffer len is limited to u32 max.
1733 const data_size: u32 = @intCast(data_buffer.items.len);
1734 var header = try self.resourceHeader(node.id, node.type, .{
1735 .data_size = data_size,
1736 });
1737 defer header.deinit(self.allocator);
1738
1739 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1740 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
1741
1742 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1743
1744 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1745 try writeResourceData(writer, data_fbs.reader(), data_size);
1746 }
1747
1748 fn writeDialogHeaderAndStrings(
1749 self: *Compiler,
1750 node: *Node.Dialog,
1751 data_writer: anytype,
1752 resource: Resource,
1753 optional_statement_values: *const DialogOptionalStatementValues,
1754 x: Number,
1755 y: Number,
1756 width: Number,
1757 height: Number,
1758 ) !void {
1759 // Header
1760 if (resource == .dialogex) {
1761 const help_id: u32 = help_id: {
1762 if (node.help_id == null) break :help_id 0;
1763 break :help_id evaluateNumberExpression(node.help_id.?, self.source, self.input_code_pages).value;
1764 };
1765 try data_writer.writeInt(u16, 1, .little); // version number, always 1
1766 try data_writer.writeInt(u16, 0xFFFF, .little); // signature, always 0xFFFF
1767 try data_writer.writeInt(u32, help_id, .little);
1768 try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
1769 try data_writer.writeInt(u32, optional_statement_values.style, .little);
1770 } else {
1771 try data_writer.writeInt(u32, optional_statement_values.style, .little);
1772 try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
1773 }
1774 // This limit is enforced by the parser, so we know the number of controls
1775 // is within the range of a u16.
1776 try data_writer.writeInt(u16, @as(u16, @intCast(node.controls.len)), .little);
1777 try data_writer.writeInt(u16, x.asWord(), .little);
1778 try data_writer.writeInt(u16, y.asWord(), .little);
1779 try data_writer.writeInt(u16, width.asWord(), .little);
1780 try data_writer.writeInt(u16, height.asWord(), .little);
1781
1782 // Menu
1783 if (optional_statement_values.menu) |menu| {
1784 try menu.write(data_writer);
1785 } else {
1786 try data_writer.writeInt(u16, 0, .little);
1787 }
1788 // Class
1789 if (optional_statement_values.class) |class| {
1790 try class.write(data_writer);
1791 } else {
1792 try data_writer.writeInt(u16, 0, .little);
1793 }
1794 // Caption
1795 if (optional_statement_values.caption) |caption| {
1796 const parsed = try self.parseQuotedStringAsWideString(caption);
1797 defer self.allocator.free(parsed);
1798 try data_writer.writeAll(std.mem.sliceAsBytes(parsed[0 .. parsed.len + 1]));
1799 } else {
1800 try data_writer.writeInt(u16, 0, .little);
1801 }
1802 // Font
1803 if (optional_statement_values.font) |font| {
1804 try self.writeDialogFont(resource, font, data_writer);
1805 }
1806 }
1807
1808 fn writeDialogControl(
1809 self: *Compiler,
1810 control: *Node.ControlStatement,
1811 data_writer: anytype,
1812 resource: Resource,
1813 bytes_written_so_far: u32,
1814 controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),
1815 ) !void {
1816 const control_type = rc.Control.map.get(control.type.slice(self.source)).?;
1817
1818 // Each control must be at a 4-byte boundary. However, the Windows RC
1819 // compiler will miscompile controls if their extra data ends on an odd offset.
1820 // We will avoid the miscompilation and emit a warning.
1821 const num_padding = numPaddingBytesNeeded(bytes_written_so_far);
1822 if (num_padding == 1 or num_padding == 3) {
1823 try self.addErrorDetails(.{
1824 .err = .rc_would_miscompile_control_padding,
1825 .type = .warning,
1826 .token = control.type,
1827 });
1828 try self.addErrorDetails(.{
1829 .err = .rc_would_miscompile_control_padding,
1830 .type = .note,
1831 .print_source_line = false,
1832 .token = control.type,
1833 });
1834 }
1835 try data_writer.writeByteNTimes(0, num_padding);
1836
1837 const style = if (control.style) |style_expression|
1838 // Certain styles are implied by the control type
1839 evaluateFlagsExpressionWithDefault(res.ControlClass.getImpliedStyle(control_type), style_expression, self.source, self.input_code_pages)
1840 else
1841 res.ControlClass.getImpliedStyle(control_type);
1842
1843 const exstyle = if (control.exstyle) |exstyle_expression|
1844 evaluateFlagsExpressionWithDefault(0, exstyle_expression, self.source, self.input_code_pages)
1845 else
1846 0;
1847
1848 switch (resource) {
1849 .dialog => {
1850 // Note: Reverse order from DIALOGEX
1851 try data_writer.writeInt(u32, style, .little);
1852 try data_writer.writeInt(u32, exstyle, .little);
1853 },
1854 .dialogex => {
1855 const help_id: u32 = if (control.help_id) |help_id_expression|
1856 evaluateNumberExpression(help_id_expression, self.source, self.input_code_pages).value
1857 else
1858 0;
1859 try data_writer.writeInt(u32, help_id, .little);
1860 // Note: Reverse order from DIALOG
1861 try data_writer.writeInt(u32, exstyle, .little);
1862 try data_writer.writeInt(u32, style, .little);
1863 },
1864 else => unreachable,
1865 }
1866
1867 const control_x = evaluateNumberExpression(control.x, self.source, self.input_code_pages);
1868 const control_y = evaluateNumberExpression(control.y, self.source, self.input_code_pages);
1869 const control_width = evaluateNumberExpression(control.width, self.source, self.input_code_pages);
1870 const control_height = evaluateNumberExpression(control.height, self.source, self.input_code_pages);
1871
1872 try data_writer.writeInt(u16, control_x.asWord(), .little);
1873 try data_writer.writeInt(u16, control_y.asWord(), .little);
1874 try data_writer.writeInt(u16, control_width.asWord(), .little);
1875 try data_writer.writeInt(u16, control_height.asWord(), .little);
1876
1877 const control_id = evaluateNumberExpression(control.id, self.source, self.input_code_pages);
1878 switch (resource) {
1879 .dialog => try data_writer.writeInt(u16, control_id.asWord(), .little),
1880 .dialogex => try data_writer.writeInt(u32, control_id.value, .little),
1881 else => unreachable,
1882 }
1883
1884 const control_id_for_map: u32 = switch (resource) {
1885 .dialog => control_id.asWord(),
1886 .dialogex => control_id.value,
1887 else => unreachable,
1888 };
1889 const result = controls_by_id.getOrPutAssumeCapacity(control_id_for_map);
1890 if (result.found_existing) {
1891 if (!self.silent_duplicate_control_ids) {
1892 try self.addErrorDetails(.{
1893 .err = .control_id_already_defined,
1894 .type = .warning,
1895 .token = control.id.getFirstToken(),
1896 .token_span_end = control.id.getLastToken(),
1897 .extra = .{ .number = control_id_for_map },
1898 });
1899 try self.addErrorDetails(.{
1900 .err = .control_id_already_defined,
1901 .type = .note,
1902 .token = result.value_ptr.*.id.getFirstToken(),
1903 .token_span_end = result.value_ptr.*.id.getLastToken(),
1904 .extra = .{ .number = control_id_for_map },
1905 });
1906 }
1907 } else {
1908 result.value_ptr.* = control;
1909 }
1910
1911 if (res.ControlClass.fromControl(control_type)) |control_class| {
1912 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1913 try ordinal.write(data_writer);
1914 } else {
1915 const class_node = control.class.?;
1916 if (class_node.isNumberExpression()) {
1917 const number = evaluateNumberExpression(class_node, self.source, self.input_code_pages);
1918 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
1919 // This is different from how the Windows RC compiles ordinals here,
1920 // but I think that's a miscompilation/bug of the Windows implementation.
1921 // The Windows behavior is (where LSB = least significant byte):
1922 // - If the LSB is 0x00 => 0xFFFF0000
1923 // - If the LSB is < 0x80 => 0x000000<LSB>
1924 // - If the LSB is >= 0x80 => 0x0000FF<LSB>
1925 //
1926 // Because of this, we emit a warning about the potential miscompilation
1927 try self.addErrorDetails(.{
1928 .err = .rc_would_miscompile_control_class_ordinal,
1929 .type = .warning,
1930 .token = class_node.getFirstToken(),
1931 .token_span_end = class_node.getLastToken(),
1932 });
1933 try self.addErrorDetails(.{
1934 .err = .rc_would_miscompile_control_class_ordinal,
1935 .type = .note,
1936 .print_source_line = false,
1937 .token = class_node.getFirstToken(),
1938 .token_span_end = class_node.getLastToken(),
1939 });
1940 // And then write out the ordinal using a proper a NameOrOrdinal encoding.
1941 try ordinal.write(data_writer);
1942 } else if (class_node.isStringLiteral()) {
1943 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
1944 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
1945 defer self.allocator.free(parsed);
1946 if (rc.ControlClass.fromWideString(parsed)) |control_class| {
1947 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1948 try ordinal.write(data_writer);
1949 } else {
1950 // NUL acts as a terminator
1951 // TODO: Maybe warn when parsed_terminated.len != parsed.len, since
1952 // it seems unlikely that NUL-termination is something intentional
1953 const parsed_terminated = std.mem.sliceTo(parsed, 0);
1954 const name = NameOrOrdinal{ .name = parsed_terminated };
1955 try name.write(data_writer);
1956 }
1957 } else {
1958 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
1959 const literal_slice = literal_node.token.slice(self.source);
1960 // This succeeding is guaranteed by the parser
1961 const control_class = rc.ControlClass.map.get(literal_slice) orelse unreachable;
1962 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1963 try ordinal.write(data_writer);
1964 }
1965 }
1966
1967 if (control.text) |text_token| {
1968 const bytes = SourceBytes{
1969 .slice = text_token.slice(self.source),
1970 .code_page = self.input_code_pages.getForToken(text_token),
1971 };
1972 if (text_token.isStringLiteral()) {
1973 const text = try self.parseQuotedStringAsWideString(text_token);
1974 defer self.allocator.free(text);
1975 const name = NameOrOrdinal{ .name = text };
1976 try name.write(data_writer);
1977 } else {
1978 std.debug.assert(text_token.id == .number);
1979 const number = literals.parseNumberLiteral(bytes);
1980 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
1981 try ordinal.write(data_writer);
1982 }
1983 } else {
1984 try NameOrOrdinal.writeEmpty(data_writer);
1985 }
1986
1987 var extra_data_buf = std.ArrayList(u8).init(self.allocator);
1988 defer extra_data_buf.deinit();
1989 // The extra data byte length must be able to fit within a u16.
1990 var limited_extra_data_writer = limitedWriter(extra_data_buf.writer(), std.math.maxInt(u16));
1991 const extra_data_writer = limited_extra_data_writer.writer();
1992 for (control.extra_data) |data_expression| {
1993 const data = try self.evaluateDataExpression(data_expression);
1994 defer data.deinit(self.allocator);
1995 data.write(extra_data_writer) catch |err| switch (err) {
1996 error.NoSpaceLeft => {
1997 try self.addErrorDetails(.{
1998 .err = .control_extra_data_size_exceeds_max,
1999 .token = control.type,
2000 });
2001 return self.addErrorDetailsAndFail(.{
2002 .err = .control_extra_data_size_exceeds_max,
2003 .type = .note,
2004 .token = data_expression.getFirstToken(),
2005 .token_span_end = data_expression.getLastToken(),
2006 });
2007 },
2008 else => |e| return e,
2009 };
2010 }
2011 // We know the extra_data_buf size fits within a u16.
2012 const extra_data_size: u16 = @intCast(extra_data_buf.items.len);
2013 try data_writer.writeInt(u16, extra_data_size, .little);
2014 try data_writer.writeAll(extra_data_buf.items);
2015 }
2016
2017 pub fn writeToolbar(self: *Compiler, node: *Node.Toolbar, writer: anytype) !void {
2018 var data_buffer = std.ArrayList(u8).init(self.allocator);
2019 defer data_buffer.deinit();
2020 const data_writer = data_buffer.writer();
2021
2022 const button_width = evaluateNumberExpression(node.button_width, self.source, self.input_code_pages);
2023 const button_height = evaluateNumberExpression(node.button_height, self.source, self.input_code_pages);
2024
2025 // I'm assuming this is some sort of version
2026 // TODO: Try to find something mentioning this
2027 try data_writer.writeInt(u16, 1, .little);
2028 try data_writer.writeInt(u16, button_width.asWord(), .little);
2029 try data_writer.writeInt(u16, button_height.asWord(), .little);
2030 // Number of buttons is guaranteed by the parser to be within maxInt(u16).
2031 try data_writer.writeInt(u16, @as(u16, @intCast(node.buttons.len)), .little);
2032
2033 for (node.buttons) |button_or_sep| {
2034 switch (button_or_sep.id) {
2035 .literal => { // This is always SEPARATOR
2036 std.debug.assert(button_or_sep.cast(.literal).?.token.id == .literal);
2037 try data_writer.writeInt(u16, 0, .little);
2038 },
2039 .simple_statement => {
2040 const value_node = button_or_sep.cast(.simple_statement).?.value;
2041 const value = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
2042 try data_writer.writeInt(u16, value.asWord(), .little);
2043 },
2044 else => unreachable, // This is a bug in the parser
2045 }
2046 }
2047
2048 const data_size: u32 = @intCast(data_buffer.items.len);
2049 var header = try self.resourceHeader(node.id, node.type, .{
2050 .data_size = data_size,
2051 });
2052 defer header.deinit(self.allocator);
2053
2054 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2055
2056 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
2057
2058 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2059 try writeResourceData(writer, data_fbs.reader(), data_size);
2060 }
2061
2062 /// Weight and italic carry over from previous FONT statements within a single resource,
2063 /// so they need to be parsed ahead-of-time and stored
2064 const FontStatementValues = struct {
2065 weight: u16 = 0,
2066 italic: bool = false,
2067 node: *Node.FontStatement,
2068 };
2069
2070 pub fn writeDialogFont(self: *Compiler, resource: Resource, values: FontStatementValues, writer: anytype) !void {
2071 const node = values.node;
2072 const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);
2073 try writer.writeInt(u16, point_size.asWord(), .little);
2074
2075 if (resource == .dialogex) {
2076 try writer.writeInt(u16, values.weight, .little);
2077 }
2078
2079 if (resource == .dialogex) {
2080 try writer.writeInt(u8, @intFromBool(values.italic), .little);
2081 }
2082
2083 if (node.char_set) |char_set| {
2084 const value = evaluateNumberExpression(char_set, self.source, self.input_code_pages);
2085 try writer.writeInt(u8, @as(u8, @truncate(value.value)), .little);
2086 } else if (resource == .dialogex) {
2087 try writer.writeInt(u8, 1, .little); // DEFAULT_CHARSET
2088 }
2089
2090 const typeface = try self.parseQuotedStringAsWideString(node.typeface);
2091 defer self.allocator.free(typeface);
2092 try writer.writeAll(std.mem.sliceAsBytes(typeface[0 .. typeface.len + 1]));
2093 }
2094
2095 pub fn writeMenu(self: *Compiler, node: *Node.Menu, writer: anytype) !void {
2096 var data_buffer = std.ArrayList(u8).init(self.allocator);
2097 defer data_buffer.deinit();
2098 // The header's data length field is a u32 so limit the resource's data size so that
2099 // we know we can always specify the real size.
2100 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
2101 const data_writer = limited_writer.writer();
2102
2103 const type_bytes = SourceBytes{
2104 .slice = node.type.slice(self.source),
2105 .code_page = self.input_code_pages.getForToken(node.type),
2106 };
2107 const resource = Resource.fromString(type_bytes);
2108 std.debug.assert(resource == .menu or resource == .menuex);
2109
2110 self.writeMenuData(node, data_writer, resource) catch |err| switch (err) {
2111 error.NoSpaceLeft => {
2112 return self.addErrorDetailsAndFail(.{
2113 .err = .resource_data_size_exceeds_max,
2114 .token = node.id,
2115 });
2116 },
2117 else => |e| return e,
2118 };
2119
2120 // This intCast can't fail because the limitedWriter above guarantees that
2121 // we will never write more than maxInt(u32) bytes.
2122 const data_size: u32 = @intCast(data_buffer.items.len);
2123 var header = try self.resourceHeader(node.id, node.type, .{
2124 .data_size = data_size,
2125 });
2126 defer header.deinit(self.allocator);
2127
2128 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2129 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
2130
2131 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
2132
2133 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2134 try writeResourceData(writer, data_fbs.reader(), data_size);
2135 }
2136
2137 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
2138 /// the writer within this function could return error.NoSpaceLeft
2139 pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: anytype, resource: Resource) !void {
2140 // menu header
2141 const version: u16 = if (resource == .menu) 0 else 1;
2142 try data_writer.writeInt(u16, version, .little);
2143 const header_size: u16 = if (resource == .menu) 0 else 4;
2144 try data_writer.writeInt(u16, header_size, .little); // cbHeaderSize
2145 // Note: There can be extra bytes at the end of this header (`rgbExtra`),
2146 // but they are always zero-length for us, so we don't write anything
2147 // (the length of the rgbExtra field is inferred from the header_size).
2148 // MENU => rgbExtra: [cbHeaderSize]u8
2149 // MENUEX => rgbExtra: [cbHeaderSize-4]u8
2150
2151 if (resource == .menuex) {
2152 if (node.help_id) |help_id_node| {
2153 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
2154 try data_writer.writeInt(u32, help_id.value, .little);
2155 } else {
2156 try data_writer.writeInt(u32, 0, .little);
2157 }
2158 }
2159
2160 for (node.items, 0..) |item, i| {
2161 const is_last = i == node.items.len - 1;
2162 try self.writeMenuItem(item, data_writer, is_last);
2163 }
2164 }
2165
2166 pub fn writeMenuItem(self: *Compiler, node: *Node, writer: anytype, is_last_of_parent: bool) !void {
2167 switch (node.id) {
2168 .menu_item_separator => {
2169 // This is the 'alternate compability form' of the separator, see
2170 // https://devblogs.microsoft.com/oldnewthing/20080710-00/?p=21673
2171 //
2172 // The 'correct' way is to set the MF_SEPARATOR flag, but the Win32 RC
2173 // compiler still uses this alternate form, so that's what we use too.
2174 var flags = res.MenuItemFlags{};
2175 if (is_last_of_parent) flags.markLast();
2176 try writer.writeInt(u16, flags.value, .little);
2177 try writer.writeInt(u16, 0, .little); // id
2178 try writer.writeInt(u16, 0, .little); // null-terminated UTF-16 text
2179 },
2180 .menu_item => {
2181 const menu_item = @fieldParentPtr(Node.MenuItem, "base", node);
2182 var flags = res.MenuItemFlags{};
2183 for (menu_item.option_list) |option_token| {
2184 // This failing would be a bug in the parser
2185 const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
2186 flags.apply(option);
2187 }
2188 if (is_last_of_parent) flags.markLast();
2189 try writer.writeInt(u16, flags.value, .little);
2190
2191 var result = evaluateNumberExpression(menu_item.result, self.source, self.input_code_pages);
2192 try writer.writeInt(u16, result.asWord(), .little);
2193
2194 var text = try self.parseQuotedStringAsWideString(menu_item.text);
2195 defer self.allocator.free(text);
2196 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2197 },
2198 .popup => {
2199 const popup = @fieldParentPtr(Node.Popup, "base", node);
2200 var flags = res.MenuItemFlags{ .value = res.MF.POPUP };
2201 for (popup.option_list) |option_token| {
2202 // This failing would be a bug in the parser
2203 const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
2204 flags.apply(option);
2205 }
2206 if (is_last_of_parent) flags.markLast();
2207 try writer.writeInt(u16, flags.value, .little);
2208
2209 var text = try self.parseQuotedStringAsWideString(popup.text);
2210 defer self.allocator.free(text);
2211 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2212
2213 for (popup.items, 0..) |item, i| {
2214 const is_last = i == popup.items.len - 1;
2215 try self.writeMenuItem(item, writer, is_last);
2216 }
2217 },
2218 inline .menu_item_ex, .popup_ex => |node_type| {
2219 const menu_item = @fieldParentPtr(node_type.Type(), "base", node);
2220
2221 if (menu_item.type) |flags| {
2222 const value = evaluateNumberExpression(flags, self.source, self.input_code_pages);
2223 try writer.writeInt(u32, value.value, .little);
2224 } else {
2225 try writer.writeInt(u32, 0, .little);
2226 }
2227
2228 if (menu_item.state) |state| {
2229 const value = evaluateNumberExpression(state, self.source, self.input_code_pages);
2230 try writer.writeInt(u32, value.value, .little);
2231 } else {
2232 try writer.writeInt(u32, 0, .little);
2233 }
2234
2235 if (menu_item.id) |id| {
2236 const value = evaluateNumberExpression(id, self.source, self.input_code_pages);
2237 try writer.writeInt(u32, value.value, .little);
2238 } else {
2239 try writer.writeInt(u32, 0, .little);
2240 }
2241
2242 var flags: u16 = 0;
2243 if (is_last_of_parent) flags |= comptime @as(u16, @intCast(res.MF.END));
2244 // This constant doesn't seem to have a named #define, it's different than MF_POPUP
2245 if (node_type == .popup_ex) flags |= 0x01;
2246 try writer.writeInt(u16, flags, .little);
2247
2248 var text = try self.parseQuotedStringAsWideString(menu_item.text);
2249 defer self.allocator.free(text);
2250 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2251
2252 // Only the combination of the flags u16 and the text bytes can cause
2253 // non-DWORD alignment, so we can just use the byte length of those
2254 // two values to realign to DWORD alignment.
2255 const relevant_bytes = 2 + (text.len + 1) * 2;
2256 try writeDataPadding(writer, @intCast(relevant_bytes));
2257
2258 if (node_type == .popup_ex) {
2259 if (menu_item.help_id) |help_id_node| {
2260 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
2261 try writer.writeInt(u32, help_id.value, .little);
2262 } else {
2263 try writer.writeInt(u32, 0, .little);
2264 }
2265
2266 for (menu_item.items, 0..) |item, i| {
2267 const is_last = i == menu_item.items.len - 1;
2268 try self.writeMenuItem(item, writer, is_last);
2269 }
2270 }
2271 },
2272 else => unreachable,
2273 }
2274 }
2275
2276 pub fn writeVersionInfo(self: *Compiler, node: *Node.VersionInfo, writer: anytype) !void {
2277 var data_buffer = std.ArrayList(u8).init(self.allocator);
2278 defer data_buffer.deinit();
2279 // The node's length field (which is inclusive of the length of all of its children) is a u16
2280 // so limit the node's data size so that we know we can always specify the real size.
2281 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u16));
2282 const data_writer = limited_writer.writer();
2283
2284 try data_writer.writeInt(u16, 0, .little); // placeholder size
2285 try data_writer.writeInt(u16, res.FixedFileInfo.byte_len, .little);
2286 try data_writer.writeInt(u16, res.VersionNode.type_binary, .little);
2287 const key_bytes = std.mem.sliceAsBytes(res.FixedFileInfo.key[0 .. res.FixedFileInfo.key.len + 1]);
2288 try data_writer.writeAll(key_bytes);
2289 // The number of bytes written up to this point is always the same, since the name
2290 // of the node is a constant (FixedFileInfo.key). The total number of bytes
2291 // written so far is 38, so we need 2 padding bytes to get back to DWORD alignment
2292 try data_writer.writeInt(u16, 0, .little);
2293
2294 var fixed_file_info = res.FixedFileInfo{};
2295 for (node.fixed_info) |fixed_info| {
2296 switch (fixed_info.id) {
2297 .version_statement => {
2298 const version_statement = @fieldParentPtr(Node.VersionStatement, "base", fixed_info);
2299 const version_type = rc.VersionInfo.map.get(version_statement.type.slice(self.source)).?;
2300
2301 // Ensure that all parts are cleared for each version, to properly account for
2302 // potential duplicate PRODUCTVERSION/FILEVERSION statements
2303 switch (version_type) {
2304 .file_version => @memset(&fixed_file_info.file_version.parts, 0),
2305 .product_version => @memset(&fixed_file_info.product_version.parts, 0),
2306 else => unreachable,
2307 }
2308
2309 for (version_statement.parts, 0..) |part, i| {
2310 const part_value = evaluateNumberExpression(part, self.source, self.input_code_pages);
2311 if (part_value.is_long) {
2312 try self.addErrorDetails(.{
2313 .err = .rc_would_error_u16_with_l_suffix,
2314 .type = .warning,
2315 .token = part.getFirstToken(),
2316 .token_span_end = part.getLastToken(),
2317 .extra = .{ .statement_with_u16_param = switch (version_type) {
2318 .file_version => .fileversion,
2319 .product_version => .productversion,
2320 else => unreachable,
2321 } },
2322 });
2323 try self.addErrorDetails(.{
2324 .err = .rc_would_error_u16_with_l_suffix,
2325 .print_source_line = false,
2326 .type = .note,
2327 .token = part.getFirstToken(),
2328 .token_span_end = part.getLastToken(),
2329 .extra = .{ .statement_with_u16_param = switch (version_type) {
2330 .file_version => .fileversion,
2331 .product_version => .productversion,
2332 else => unreachable,
2333 } },
2334 });
2335 }
2336 switch (version_type) {
2337 .file_version => {
2338 fixed_file_info.file_version.parts[i] = part_value.asWord();
2339 },
2340 .product_version => {
2341 fixed_file_info.product_version.parts[i] = part_value.asWord();
2342 },
2343 else => unreachable,
2344 }
2345 }
2346 },
2347 .simple_statement => {
2348 const statement = @fieldParentPtr(Node.SimpleStatement, "base", fixed_info);
2349 const statement_type = rc.VersionInfo.map.get(statement.identifier.slice(self.source)).?;
2350 const value = evaluateNumberExpression(statement.value, self.source, self.input_code_pages);
2351 switch (statement_type) {
2352 .file_flags_mask => fixed_file_info.file_flags_mask = value.value,
2353 .file_flags => fixed_file_info.file_flags = value.value,
2354 .file_os => fixed_file_info.file_os = value.value,
2355 .file_type => fixed_file_info.file_type = value.value,
2356 .file_subtype => fixed_file_info.file_subtype = value.value,
2357 else => unreachable,
2358 }
2359 },
2360 else => unreachable,
2361 }
2362 }
2363 try fixed_file_info.write(data_writer);
2364
2365 for (node.block_statements) |statement| {
2366 self.writeVersionNode(statement, data_writer, &data_buffer) catch |err| switch (err) {
2367 error.NoSpaceLeft => {
2368 try self.addErrorDetails(.{
2369 .err = .version_node_size_exceeds_max,
2370 .token = node.id,
2371 });
2372 return self.addErrorDetailsAndFail(.{
2373 .err = .version_node_size_exceeds_max,
2374 .type = .note,
2375 .token = statement.getFirstToken(),
2376 .token_span_end = statement.getLastToken(),
2377 });
2378 },
2379 else => |e| return e,
2380 };
2381 }
2382
2383 // We know that data_buffer.items.len is within the limits of a u16, since we
2384 // limited the writer to maxInt(u16)
2385 const data_size: u16 = @intCast(data_buffer.items.len);
2386 // And now that we know the full size of this node (including its children), set its size
2387 std.mem.writeInt(u16, data_buffer.items[0..2], data_size, .little);
2388
2389 var header = try self.resourceHeader(node.id, node.versioninfo, .{
2390 .data_size = data_size,
2391 });
2392 defer header.deinit(self.allocator);
2393
2394 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2395
2396 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
2397
2398 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2399 try writeResourceData(writer, data_fbs.reader(), data_size);
2400 }
2401
2402 /// Expects writer to be a LimitedWriter limited to u16, meaning all writes to
2403 /// the writer within this function could return error.NoSpaceLeft, and that buf.items.len
2404 /// will never be able to exceed maxInt(u16).
2405 pub fn writeVersionNode(self: *Compiler, node: *Node, writer: anytype, buf: *std.ArrayList(u8)) !void {
2406 // We can assume that buf.items.len will never be able to exceed the limits of a u16
2407 try writeDataPadding(writer, @as(u16, @intCast(buf.items.len)));
2408
2409 const node_and_children_size_offset = buf.items.len;
2410 try writer.writeInt(u16, 0, .little); // placeholder for size
2411 const data_size_offset = buf.items.len;
2412 try writer.writeInt(u16, 0, .little); // placeholder for data size
2413 const data_type_offset = buf.items.len;
2414 // Data type is string unless the node contains values that are numbers.
2415 try writer.writeInt(u16, res.VersionNode.type_string, .little);
2416
2417 switch (node.id) {
2418 inline .block, .block_value => |node_type| {
2419 const block_or_value = @fieldParentPtr(node_type.Type(), "base", node);
2420 const parsed_key = try self.parseQuotedStringAsWideString(block_or_value.key);
2421 defer self.allocator.free(parsed_key);
2422
2423 const parsed_key_to_first_null = std.mem.sliceTo(parsed_key, 0);
2424 try writer.writeAll(std.mem.sliceAsBytes(parsed_key_to_first_null[0 .. parsed_key_to_first_null.len + 1]));
2425
2426 var has_number_value: bool = false;
2427 for (block_or_value.values) |value_value_node_uncasted| {
2428 const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
2429 if (value_value_node.expression.isNumberExpression()) {
2430 has_number_value = true;
2431 break;
2432 }
2433 }
2434 // The units used here are dependent on the type. If there are any numbers, then
2435 // this is a byte count. If there are only strings, then this is a count of
2436 // UTF-16 code units.
2437 //
2438 // The Win32 RC compiler miscompiles this count in the case of values that
2439 // have a mix of numbers and strings. This is detected and a warning is emitted
2440 // during parsing, so we can just do the correct thing here.
2441 var values_size: usize = 0;
2442
2443 try writeDataPadding(writer, @intCast(buf.items.len));
2444
2445 for (block_or_value.values, 0..) |value_value_node_uncasted, i| {
2446 const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
2447 const value_node = value_value_node.expression;
2448 if (value_node.isNumberExpression()) {
2449 const number = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
2450 // This is used to write u16 or u32 depending on the number's suffix
2451 const data_wrapper = Data{ .number = number };
2452 try data_wrapper.write(writer);
2453 // Numbers use byte count
2454 values_size += if (number.is_long) 4 else 2;
2455 } else {
2456 std.debug.assert(value_node.isStringLiteral());
2457 const literal_node = value_node.cast(.literal).?;
2458 const parsed_value = try self.parseQuotedStringAsWideString(literal_node.token);
2459 defer self.allocator.free(parsed_value);
2460
2461 const parsed_to_first_null = std.mem.sliceTo(parsed_value, 0);
2462 try writer.writeAll(std.mem.sliceAsBytes(parsed_to_first_null));
2463 // Strings use UTF-16 code-unit count including the null-terminator, but
2464 // only if there are no number values in the list.
2465 var value_size = parsed_to_first_null.len;
2466 if (has_number_value) value_size *= 2; // 2 bytes per UTF-16 code unit
2467 values_size += value_size;
2468 // The null-terminator is only included if there's a trailing comma
2469 // or this is the last value. If the value evaluates to empty, then
2470 // it never gets a null terminator. If there was an explicit null-terminator
2471 // in the string, we still need to potentially add one since we already
2472 // sliced to the terminator.
2473 const is_last = i == block_or_value.values.len - 1;
2474 const is_empty = parsed_to_first_null.len == 0;
2475 const is_only = block_or_value.values.len == 1;
2476 if ((!is_empty or !is_only) and (is_last or value_value_node.trailing_comma)) {
2477 try writer.writeInt(u16, 0, .little);
2478 values_size += if (has_number_value) 2 else 1;
2479 }
2480 }
2481 }
2482 var data_size_slice = buf.items[data_size_offset..];
2483 std.mem.writeInt(u16, data_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(values_size)), .little);
2484
2485 if (has_number_value) {
2486 const data_type_slice = buf.items[data_type_offset..];
2487 std.mem.writeInt(u16, data_type_slice[0..@sizeOf(u16)], res.VersionNode.type_binary, .little);
2488 }
2489
2490 if (node_type == .block) {
2491 const block = block_or_value;
2492 for (block.children) |child| {
2493 try self.writeVersionNode(child, writer, buf);
2494 }
2495 }
2496 },
2497 else => unreachable,
2498 }
2499
2500 const node_and_children_size = buf.items.len - node_and_children_size_offset;
2501 const node_and_children_size_slice = buf.items[node_and_children_size_offset..];
2502 std.mem.writeInt(u16, node_and_children_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(node_and_children_size)), .little);
2503 }
2504
2505 pub fn writeStringTable(self: *Compiler, node: *Node.StringTable) !void {
2506 const language = getLanguageFromOptionalStatements(node.optional_statements, self.source, self.input_code_pages) orelse self.state.language;
2507
2508 for (node.strings) |string_node| {
2509 const string = @fieldParentPtr(Node.StringTableString, "base", string_node);
2510 const string_id_data = try self.evaluateDataExpression(string.id);
2511 const string_id = string_id_data.number.asWord();
2512
2513 self.state.string_tables.set(
2514 self.arena,
2515 language,
2516 string_id,
2517 string.string,
2518 &node.base,
2519 self.source,
2520 self.input_code_pages,
2521 self.state.version,
2522 self.state.characteristics,
2523 ) catch |err| switch (err) {
2524 error.StringAlreadyDefined => {
2525 // It might be nice to have these errors point to the ids rather than the
2526 // string tokens, but that would mean storing the id token of each string
2527 // which doesn't seem worth it just for slightly better error messages.
2528 try self.addErrorDetails(ErrorDetails{
2529 .err = .string_already_defined,
2530 .token = string.string,
2531 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
2532 });
2533 const existing_def_table = self.state.string_tables.tables.getPtr(language).?;
2534 const existing_definition = existing_def_table.get(string_id).?;
2535 return self.addErrorDetailsAndFail(ErrorDetails{
2536 .err = .string_already_defined,
2537 .type = .note,
2538 .token = existing_definition,
2539 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
2540 });
2541 },
2542 error.OutOfMemory => |e| return e,
2543 };
2544 }
2545 }
2546
2547 /// Expects this to be a top-level LANGUAGE statement
2548 pub fn writeLanguageStatement(self: *Compiler, node: *Node.LanguageStatement) void {
2549 const primary = Compiler.evaluateNumberExpression(node.primary_language_id, self.source, self.input_code_pages);
2550 const sublanguage = Compiler.evaluateNumberExpression(node.sublanguage_id, self.source, self.input_code_pages);
2551 self.state.language.primary_language_id = @truncate(primary.value);
2552 self.state.language.sublanguage_id = @truncate(sublanguage.value);
2553 }
2554
2555 /// Expects this to be a top-level VERSION or CHARACTERISTICS statement
2556 pub fn writeTopLevelSimpleStatement(self: *Compiler, node: *Node.SimpleStatement) void {
2557 const value = Compiler.evaluateNumberExpression(node.value, self.source, self.input_code_pages);
2558 const statement_type = rc.TopLevelKeywords.map.get(node.identifier.slice(self.source)).?;
2559 switch (statement_type) {
2560 .characteristics => self.state.characteristics = value.value,
2561 .version => self.state.version = value.value,
2562 else => unreachable,
2563 }
2564 }
2565
2566 pub const ResourceHeaderOptions = struct {
2567 language: ?res.Language = null,
2568 data_size: DWORD = 0,
2569 };
2570
2571 pub fn resourceHeader(self: *Compiler, id_token: Token, type_token: Token, options: ResourceHeaderOptions) !ResourceHeader {
2572 const id_bytes = self.sourceBytesForToken(id_token);
2573 const type_bytes = self.sourceBytesForToken(type_token);
2574 return ResourceHeader.init(
2575 self.allocator,
2576 id_bytes,
2577 type_bytes,
2578 options.data_size,
2579 options.language orelse self.state.language,
2580 self.state.version,
2581 self.state.characteristics,
2582 ) catch |err| switch (err) {
2583 error.OutOfMemory => |e| return e,
2584 error.TypeNonAsciiOrdinal => {
2585 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes).?;
2586 try self.addErrorDetails(.{
2587 .err = .invalid_digit_character_in_ordinal,
2588 .type = .err,
2589 .token = type_token,
2590 });
2591 return self.addErrorDetailsAndFail(.{
2592 .err = .win32_non_ascii_ordinal,
2593 .type = .note,
2594 .token = type_token,
2595 .print_source_line = false,
2596 .extra = .{ .number = win32_rc_ordinal.ordinal },
2597 });
2598 },
2599 error.IdNonAsciiOrdinal => {
2600 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes).?;
2601 try self.addErrorDetails(.{
2602 .err = .invalid_digit_character_in_ordinal,
2603 .type = .err,
2604 .token = id_token,
2605 });
2606 return self.addErrorDetailsAndFail(.{
2607 .err = .win32_non_ascii_ordinal,
2608 .type = .note,
2609 .token = id_token,
2610 .print_source_line = false,
2611 .extra = .{ .number = win32_rc_ordinal.ordinal },
2612 });
2613 },
2614 };
2615 }
2616
2617 pub const ResourceHeader = struct {
2618 name_value: NameOrOrdinal,
2619 type_value: NameOrOrdinal,
2620 language: res.Language,
2621 memory_flags: MemoryFlags,
2622 data_size: DWORD,
2623 version: DWORD,
2624 characteristics: DWORD,
2625 data_version: DWORD = 0,
2626
2627 pub const InitError = error{ OutOfMemory, IdNonAsciiOrdinal, TypeNonAsciiOrdinal };
2628
2629 pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {
2630 const type_value = type: {
2631 const resource_type = Resource.fromString(type_bytes);
2632 if (res.RT.fromResource(resource_type)) |rt_constant| {
2633 break :type NameOrOrdinal{ .ordinal = @intFromEnum(rt_constant) };
2634 } else {
2635 break :type try NameOrOrdinal.fromString(allocator, type_bytes);
2636 }
2637 };
2638 errdefer type_value.deinit(allocator);
2639 if (type_value == .name) {
2640 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes)) |_| {
2641 return error.TypeNonAsciiOrdinal;
2642 }
2643 }
2644
2645 const name_value = try NameOrOrdinal.fromString(allocator, id_bytes);
2646 errdefer name_value.deinit(allocator);
2647 if (name_value == .name) {
2648 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes)) |_| {
2649 return error.IdNonAsciiOrdinal;
2650 }
2651 }
2652
2653 const predefined_resource_type = type_value.predefinedResourceType();
2654
2655 return ResourceHeader{
2656 .name_value = name_value,
2657 .type_value = type_value,
2658 .data_size = data_size,
2659 .memory_flags = MemoryFlags.defaults(predefined_resource_type),
2660 .language = language,
2661 .version = version,
2662 .characteristics = characteristics,
2663 };
2664 }
2665
2666 pub fn deinit(self: ResourceHeader, allocator: Allocator) void {
2667 self.name_value.deinit(allocator);
2668 self.type_value.deinit(allocator);
2669 }
2670
2671 pub const SizeInfo = struct {
2672 bytes: u32,
2673 padding_after_name: u2,
2674 };
2675
2676 fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {
2677 var header_size: u32 = 8;
2678 header_size = try std.math.add(
2679 u32,
2680 header_size,
2681 std.math.cast(u32, self.name_value.byteLen()) orelse return error.Overflow,
2682 );
2683 header_size = try std.math.add(
2684 u32,
2685 header_size,
2686 std.math.cast(u32, self.type_value.byteLen()) orelse return error.Overflow,
2687 );
2688 const padding_after_name = numPaddingBytesNeeded(header_size);
2689 header_size = try std.math.add(u32, header_size, padding_after_name);
2690 header_size = try std.math.add(u32, header_size, 16);
2691 return .{ .bytes = header_size, .padding_after_name = padding_after_name };
2692 }
2693
2694 pub fn writeAssertNoOverflow(self: ResourceHeader, writer: anytype) !void {
2695 return self.writeSizeInfo(writer, self.calcSize() catch unreachable);
2696 }
2697
2698 pub fn write(self: ResourceHeader, writer: anytype, err_ctx: errors.DiagnosticsContext) !void {
2699 const size_info = self.calcSize() catch {
2700 try err_ctx.diagnostics.append(.{
2701 .err = .resource_data_size_exceeds_max,
2702 .token = err_ctx.token,
2703 });
2704 return error.CompileError;
2705 };
2706 return self.writeSizeInfo(writer, size_info);
2707 }
2708
2709 fn writeSizeInfo(self: ResourceHeader, writer: anytype, size_info: SizeInfo) !void {
2710 try writer.writeInt(DWORD, self.data_size, .little); // DataSize
2711 try writer.writeInt(DWORD, size_info.bytes, .little); // HeaderSize
2712 try self.type_value.write(writer); // TYPE
2713 try self.name_value.write(writer); // NAME
2714 try writer.writeByteNTimes(0, size_info.padding_after_name);
2715
2716 try writer.writeInt(DWORD, self.data_version, .little); // DataVersion
2717 try writer.writeInt(WORD, self.memory_flags.value, .little); // MemoryFlags
2718 try writer.writeInt(WORD, self.language.asInt(), .little); // LanguageId
2719 try writer.writeInt(DWORD, self.version, .little); // Version
2720 try writer.writeInt(DWORD, self.characteristics, .little); // Characteristics
2721 }
2722
2723 pub fn predefinedResourceType(self: ResourceHeader) ?res.RT {
2724 return self.type_value.predefinedResourceType();
2725 }
2726
2727 pub fn applyMemoryFlags(self: *ResourceHeader, tokens: []Token, source: []const u8) void {
2728 applyToMemoryFlags(&self.memory_flags, tokens, source);
2729 }
2730
2731 pub fn applyOptionalStatements(self: *ResourceHeader, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
2732 applyToOptionalStatements(&self.language, &self.version, &self.characteristics, statements, source, code_page_lookup);
2733 }
2734 };
2735
2736 fn applyToMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
2737 for (tokens) |token| {
2738 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2739 flags.set(attribute);
2740 }
2741 }
2742
2743 /// RT_GROUP_ICON and RT_GROUP_CURSOR have their own special rules for memory flags
2744 fn applyToGroupMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
2745 // There's probably a cleaner implementation of this, but this will result in the same
2746 // flags as the Win32 RC compiler for all 986,410 K-permutations of memory flags
2747 // for an ICON resource.
2748 //
2749 // This was arrived at by iterating over the permutations and creating a
2750 // list where each line looks something like this:
2751 // MOVEABLE PRELOAD -> 0x1050 (MOVEABLE|PRELOAD|DISCARDABLE)
2752 //
2753 // and then noticing a few things:
2754
2755 // 1. Any permutation that does not have PRELOAD in it just uses the
2756 // default flags.
2757 const initial_flags = flags.*;
2758 var flags_set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
2759 for (tokens) |token| {
2760 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2761 flags_set.insert(attribute);
2762 }
2763 if (!flags_set.contains(.preload)) return;
2764
2765 // 2. Any permutation of flags where applying only the PRELOAD and LOADONCALL flags
2766 // results in no actual change by the end will just use the default flags.
2767 // For example, `PRELOAD LOADONCALL` will result in default flags, but
2768 // `LOADONCALL PRELOAD` will have PRELOAD set after they are both applied in order.
2769 for (tokens) |token| {
2770 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2771 switch (attribute) {
2772 .preload, .loadoncall => flags.set(attribute),
2773 else => {},
2774 }
2775 }
2776 if (flags.value == initial_flags.value) return;
2777
2778 // 3. If none of DISCARDABLE, SHARED, or PURE is specified, then PRELOAD
2779 // implies `flags &= ~SHARED` and LOADONCALL implies `flags |= SHARED`
2780 const shared_set = comptime blk: {
2781 var set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
2782 set.insert(.discardable);
2783 set.insert(.shared);
2784 set.insert(.pure);
2785 break :blk set;
2786 };
2787 const discardable_shared_or_pure_specified = flags_set.intersectWith(shared_set).count() != 0;
2788 for (tokens) |token| {
2789 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2790 flags.setGroup(attribute, !discardable_shared_or_pure_specified);
2791 }
2792 }
2793
2794 /// Only handles the 'base' optional statements that are shared between resource types.
2795 fn applyToOptionalStatements(language: *res.Language, version: *u32, characteristics: *u32, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
2796 for (statements) |node| switch (node.id) {
2797 .language_statement => {
2798 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
2799 language.* = languageFromLanguageStatement(language_statement, source, code_page_lookup);
2800 },
2801 .simple_statement => {
2802 const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", node);
2803 const statement_type = rc.OptionalStatements.map.get(simple_statement.identifier.slice(source)) orelse continue;
2804 const result = Compiler.evaluateNumberExpression(simple_statement.value, source, code_page_lookup);
2805 switch (statement_type) {
2806 .version => version.* = result.value,
2807 .characteristics => characteristics.* = result.value,
2808 else => unreachable, // only VERSION and CHARACTERISTICS should be in an optional statements list
2809 }
2810 },
2811 else => {},
2812 };
2813 }
2814
2815 pub fn languageFromLanguageStatement(language_statement: *const Node.LanguageStatement, source: []const u8, code_page_lookup: *const CodePageLookup) res.Language {
2816 const primary = Compiler.evaluateNumberExpression(language_statement.primary_language_id, source, code_page_lookup);
2817 const sublanguage = Compiler.evaluateNumberExpression(language_statement.sublanguage_id, source, code_page_lookup);
2818 return .{
2819 .primary_language_id = @truncate(primary.value),
2820 .sublanguage_id = @truncate(sublanguage.value),
2821 };
2822 }
2823
2824 pub fn getLanguageFromOptionalStatements(statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) ?res.Language {
2825 for (statements) |node| switch (node.id) {
2826 .language_statement => {
2827 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
2828 return languageFromLanguageStatement(language_statement, source, code_page_lookup);
2829 },
2830 else => continue,
2831 };
2832 return null;
2833 }
2834
2835 pub fn writeEmptyResource(writer: anytype) !void {
2836 const header = ResourceHeader{
2837 .name_value = .{ .ordinal = 0 },
2838 .type_value = .{ .ordinal = 0 },
2839 .language = .{
2840 .primary_language_id = 0,
2841 .sublanguage_id = 0,
2842 },
2843 .memory_flags = .{ .value = 0 },
2844 .data_size = 0,
2845 .version = 0,
2846 .characteristics = 0,
2847 };
2848 try header.writeAssertNoOverflow(writer);
2849 }
2850
2851 pub fn sourceBytesForToken(self: *Compiler, token: Token) SourceBytes {
2852 return .{
2853 .slice = token.slice(self.source),
2854 .code_page = self.input_code_pages.getForToken(token),
2855 };
2856 }
2857
2858 /// Helper that calls parseQuotedStringAsWideString with the relevant context
2859 /// Resulting slice is allocated by `self.allocator`.
2860 pub fn parseQuotedStringAsWideString(self: *Compiler, token: Token) ![:0]u16 {
2861 return literals.parseQuotedStringAsWideString(
2862 self.allocator,
2863 self.sourceBytesForToken(token),
2864 .{
2865 .start_column = token.calculateColumn(self.source, 8, null),
2866 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
2867 },
2868 );
2869 }
2870
2871 fn addErrorDetails(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
2872 try self.diagnostics.append(details);
2873 }
2874
2875 fn addErrorDetailsAndFail(self: *Compiler, details: ErrorDetails) error{ CompileError, OutOfMemory } {
2876 try self.addErrorDetails(details);
2877 return error.CompileError;
2878 }
2879};
2880
2881pub const OpenSearchPathError = std.fs.Dir.OpenError;
2882
2883fn openSearchPathDir(dir: std.fs.Dir, path: []const u8) OpenSearchPathError!std.fs.Dir {
2884 // Validate the search path to avoid possible unreachable on invalid paths,
2885 // see https://github.com/ziglang/zig/issues/15607 for why this is currently necessary.
2886 try validateSearchPath(path);
2887 return dir.openDir(path, .{});
2888}
2889
2890/// Very crude attempt at validating a path. This is imperfect
2891/// and AFAIK it is effectively impossible to implement perfect path
2892/// validation, since it ultimately depends on the underlying filesystem.
2893/// Note that this function won't be necessary if/when
2894/// https://github.com/ziglang/zig/issues/15607
2895/// is accepted/implemented.
2896fn validateSearchPath(path: []const u8) error{BadPathName}!void {
2897 switch (builtin.os.tag) {
2898 .windows => {
2899 // This will return error.BadPathName on non-Win32 namespaced paths
2900 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
2901 // Those path types are something of an unavoidable way to
2902 // still hit unreachable during the openDir call.
2903 var component_iterator = try std.fs.path.componentIterator(path);
2904 while (component_iterator.next()) |component| {
2905 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2906 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
2907 }
2908 },
2909 else => {
2910 if (std.mem.indexOfScalar(u8, path, 0) != null) return error.BadPathName;
2911 },
2912 }
2913}
2914
2915pub const SearchDir = struct {
2916 dir: std.fs.Dir,
2917 path: ?[]const u8,
2918
2919 pub fn deinit(self: *SearchDir, allocator: Allocator) void {
2920 self.dir.close();
2921 if (self.path) |path| {
2922 allocator.free(path);
2923 }
2924 }
2925};
2926
2927/// Slurps the first `size` bytes read into `slurped_header`
2928pub fn HeaderSlurpingReader(comptime size: usize, comptime ReaderType: anytype) type {
2929 return struct {
2930 child_reader: ReaderType,
2931 bytes_read: usize = 0,
2932 slurped_header: [size]u8 = [_]u8{0x00} ** size,
2933
2934 pub const Error = ReaderType.Error;
2935 pub const Reader = std.io.Reader(*@This(), Error, read);
2936
2937 pub fn read(self: *@This(), buf: []u8) Error!usize {
2938 const amt = try self.child_reader.read(buf);
2939 if (self.bytes_read < size) {
2940 const bytes_to_add = @min(amt, size - self.bytes_read);
2941 const end_index = self.bytes_read + bytes_to_add;
2942 @memcpy(self.slurped_header[self.bytes_read..end_index], buf[0..bytes_to_add]);
2943 }
2944 self.bytes_read +|= amt;
2945 return amt;
2946 }
2947
2948 pub fn reader(self: *@This()) Reader {
2949 return .{ .context = self };
2950 }
2951 };
2952}
2953
2954pub fn headerSlurpingReader(comptime size: usize, reader: anytype) HeaderSlurpingReader(size, @TypeOf(reader)) {
2955 return .{ .child_reader = reader };
2956}
2957
2958/// Sort of like std.io.LimitedReader, but a Writer.
2959/// Returns an error if writing the requested number of bytes
2960/// would ever exceed bytes_left, i.e. it does not always
2961/// write up to the limit and instead will error if the
2962/// limit would be breached if the entire slice was written.
2963pub fn LimitedWriter(comptime WriterType: type) type {
2964 return struct {
2965 inner_writer: WriterType,
2966 bytes_left: u64,
2967
2968 pub const Error = error{NoSpaceLeft} || WriterType.Error;
2969 pub const Writer = std.io.Writer(*Self, Error, write);
2970
2971 const Self = @This();
2972
2973 pub fn write(self: *Self, bytes: []const u8) Error!usize {
2974 if (bytes.len > self.bytes_left) return error.NoSpaceLeft;
2975 const amt = try self.inner_writer.write(bytes);
2976 self.bytes_left -= amt;
2977 return amt;
2978 }
2979
2980 pub fn writer(self: *Self) Writer {
2981 return .{ .context = self };
2982 }
2983 };
2984}
2985
2986/// Returns an initialised `LimitedWriter`
2987/// `bytes_left` is a `u64` to be able to take 64 bit file offsets
2988pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) {
2989 return .{ .inner_writer = inner_writer, .bytes_left = bytes_left };
2990}
2991
2992test "limitedWriter basic usage" {
2993 var buf: [4]u8 = undefined;
2994 var fbs = std.io.fixedBufferStream(&buf);
2995 var limited_stream = limitedWriter(fbs.writer(), 4);
2996 var writer = limited_stream.writer();
2997
2998 try std.testing.expectEqual(@as(usize, 3), try writer.write("123"));
2999 try std.testing.expectEqualSlices(u8, "123", buf[0..3]);
3000 try std.testing.expectError(error.NoSpaceLeft, writer.write("45"));
3001 try std.testing.expectEqual(@as(usize, 1), try writer.write("4"));
3002 try std.testing.expectEqualSlices(u8, "1234", buf[0..4]);
3003 try std.testing.expectError(error.NoSpaceLeft, writer.write("5"));
3004}
3005
3006pub const FontDir = struct {
3007 fonts: std.ArrayListUnmanaged(Font) = .{},
3008 /// To keep track of which ids are set and where they were set from
3009 ids: std.AutoHashMapUnmanaged(u16, Token) = .{},
3010
3011 pub const Font = struct {
3012 id: u16,
3013 header_bytes: [148]u8,
3014 };
3015
3016 pub fn deinit(self: *FontDir, allocator: Allocator) void {
3017 self.fonts.deinit(allocator);
3018 }
3019
3020 pub fn add(self: *FontDir, allocator: Allocator, font: Font, id_token: Token) !void {
3021 try self.ids.putNoClobber(allocator, font.id, id_token);
3022 try self.fonts.append(allocator, font);
3023 }
3024
3025 pub fn writeResData(self: *FontDir, compiler: *Compiler, writer: anytype) !void {
3026 if (self.fonts.items.len == 0) return;
3027
3028 // We know the number of fonts is limited to maxInt(u16) because fonts
3029 // must have a valid and unique u16 ordinal ID (trying to specify a FONT
3030 // with e.g. id 65537 will wrap around to 1 and be ignored if there's already
3031 // a font with that ID in the file).
3032 const num_fonts: u16 = @intCast(self.fonts.items.len);
3033
3034 // u16 count + [(u16 id + 150 bytes) for each font]
3035 // Note: This works out to a maximum data_size of 9,961,322.
3036 const data_size: u32 = 2 + (2 + 150) * num_fonts;
3037
3038 var header = Compiler.ResourceHeader{
3039 .name_value = try NameOrOrdinal.nameFromString(compiler.allocator, .{ .slice = "FONTDIR", .code_page = .windows1252 }),
3040 .type_value = NameOrOrdinal{ .ordinal = @intFromEnum(res.RT.FONTDIR) },
3041 .memory_flags = res.MemoryFlags.defaults(res.RT.FONTDIR),
3042 .language = compiler.state.language,
3043 .version = compiler.state.version,
3044 .characteristics = compiler.state.characteristics,
3045 .data_size = data_size,
3046 };
3047 defer header.deinit(compiler.allocator);
3048
3049 try header.writeAssertNoOverflow(writer);
3050 try writer.writeInt(u16, num_fonts, .little);
3051 for (self.fonts.items) |font| {
3052 // The format of the FONTDIR is a strange beast.
3053 // Technically, each FONT is seemingly meant to be written as a
3054 // FONTDIRENTRY with two trailing NUL-terminated strings corresponding to
3055 // the 'device name' and 'face name' of the .FNT file, but:
3056 //
3057 // 1. When dealing with .FNT files, the Win32 implementation
3058 // gets the device name and face name from the wrong locations,
3059 // so it's basically never going to write the real device/face name
3060 // strings.
3061 // 2. When dealing with files 76-140 bytes long, the Win32 implementation
3062 // can just crash (if there are no NUL bytes in the file).
3063 // 3. The 32-bit Win32 rc.exe uses a 148 byte size for the portion of
3064 // the FONTDIRENTRY before the NUL-terminated strings, which
3065 // does not match the documented FONTDIRENTRY size that (presumably)
3066 // this format is meant to be using, so anything iterating the
3067 // FONTDIR according to the available documentation will get bogus results.
3068 // 4. The FONT resource can be used for non-.FNT types like TTF and OTF,
3069 // in which case emulating the Win32 behavior of unconditionally
3070 // interpreting the bytes as a .FNT and trying to grab device/face names
3071 // from random bytes in the TTF/OTF file can lead to weird behavior
3072 // and errors in the Win32 implementation (for example, the device/face
3073 // name fields are offsets into the file where the NUL-terminated
3074 // string is located, but the Win32 implementation actually treats
3075 // them as signed so if they are negative then the Win32 implementation
3076 // will error; this happening for TTF fonts would just be a bug
3077 // since the TTF could otherwise be valid)
3078 // 5. The FONTDIR resource doesn't actually seem to be used at all by
3079 // anything that I've found, and instead in Windows 3.0 and newer
3080 // it seems like the FONT resources are always just iterated/accessed
3081 // directly without ever looking at the FONTDIR.
3082 //
3083 // All of these combined means that we:
3084 // - Do not need or want to emulate Win32 behavior here
3085 // - For maximum simplicity and compatibility, we just write the first
3086 // 148 bytes of the file without any interpretation (padded with
3087 // zeroes to get up to 148 bytes if necessary), and then
3088 // unconditionally write two NUL bytes, meaning that we always
3089 // write 'device name' and 'face name' as if they were 0-length
3090 // strings.
3091 //
3092 // This gives us byte-for-byte .RES compatibility in the common case while
3093 // allowing us to avoid any erroneous errors caused by trying to read
3094 // the face/device name from a bogus location. Note that the Win32
3095 // implementation never actually writes the real device/face name here
3096 // anyway (except in the bizarre case that a .FNT file has the proper
3097 // device/face name offsets within a reserved section of the .FNT file)
3098 // so there's no feasible way that anything can actually think that the
3099 // device name/face name in the FONTDIR is reliable.
3100
3101 // First, the ID is written, though
3102 try writer.writeInt(u16, font.id, .little);
3103 try writer.writeAll(&font.header_bytes);
3104 try writer.writeByteNTimes(0, 2);
3105 }
3106 try Compiler.writeDataPadding(writer, data_size);
3107 }
3108};
3109
3110pub const StringTablesByLanguage = struct {
3111 /// String tables for each language are written to the .res file in order depending on
3112 /// when the first STRINGTABLE for the language was defined, and all blocks for a given
3113 /// language are written contiguously.
3114 /// Using an ArrayHashMap here gives us this property for free.
3115 tables: std.AutoArrayHashMapUnmanaged(res.Language, StringTable) = .{},
3116
3117 pub fn deinit(self: *StringTablesByLanguage, allocator: Allocator) void {
3118 self.tables.deinit(allocator);
3119 }
3120
3121 pub fn set(
3122 self: *StringTablesByLanguage,
3123 allocator: Allocator,
3124 language: res.Language,
3125 id: u16,
3126 string_token: Token,
3127 node: *Node,
3128 source: []const u8,
3129 code_page_lookup: *const CodePageLookup,
3130 version: u32,
3131 characteristics: u32,
3132 ) StringTable.SetError!void {
3133 var get_or_put_result = try self.tables.getOrPut(allocator, language);
3134 if (!get_or_put_result.found_existing) {
3135 get_or_put_result.value_ptr.* = StringTable{};
3136 }
3137 return get_or_put_result.value_ptr.set(allocator, id, string_token, node, source, code_page_lookup, version, characteristics);
3138 }
3139};
3140
3141pub const StringTable = struct {
3142 /// Blocks are written to the .res file in order depending on when the first string
3143 /// was added to the block (i.e. `STRINGTABLE { 16 "b" 0 "a" }` would then get written
3144 /// with block ID 2 (the one with "b") first and block ID 1 (the one with "a") second).
3145 /// Using an ArrayHashMap here gives us this property for free.
3146 blocks: std.AutoArrayHashMapUnmanaged(u16, Block) = .{},
3147
3148 pub const Block = struct {
3149 strings: std.ArrayListUnmanaged(Token) = .{},
3150 set_indexes: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 },
3151 memory_flags: MemoryFlags = MemoryFlags.defaults(res.RT.STRING),
3152 characteristics: u32,
3153 version: u32,
3154
3155 /// Returns the index to insert the string into the `strings` list.
3156 /// Returns null if the string should be appended.
3157 fn getInsertionIndex(self: *Block, index: u8) ?u8 {
3158 std.debug.assert(!self.set_indexes.isSet(index));
3159
3160 const first_set = self.set_indexes.findFirstSet() orelse return null;
3161 if (first_set > index) return 0;
3162
3163 const last_set = 15 - @clz(self.set_indexes.mask);
3164 if (index > last_set) return null;
3165
3166 var bit = first_set + 1;
3167 var insertion_index: u8 = 1;
3168 while (bit != index) : (bit += 1) {
3169 if (self.set_indexes.isSet(bit)) insertion_index += 1;
3170 }
3171 return insertion_index;
3172 }
3173
3174 fn getTokenIndex(self: *Block, string_index: u8) ?u8 {
3175 const count = self.strings.items.len;
3176 if (count == 0) return null;
3177 if (count == 1) return 0;
3178
3179 const first_set = self.set_indexes.findFirstSet() orelse unreachable;
3180 if (first_set == string_index) return 0;
3181 const last_set = 15 - @clz(self.set_indexes.mask);
3182 if (last_set == string_index) return @intCast(count - 1);
3183
3184 if (first_set == last_set) return null;
3185
3186 var bit = first_set + 1;
3187 var token_index: u8 = 1;
3188 while (bit < last_set) : (bit += 1) {
3189 if (!self.set_indexes.isSet(bit)) continue;
3190 if (bit == string_index) return token_index;
3191 token_index += 1;
3192 }
3193 return null;
3194 }
3195
3196 fn dump(self: *Block) void {
3197 var bit_it = self.set_indexes.iterator(.{});
3198 var string_index: usize = 0;
3199 while (bit_it.next()) |bit_index| {
3200 const token = self.strings.items[string_index];
3201 std.debug.print("{}: [{}] {any}\n", .{ bit_index, string_index, token });
3202 string_index += 1;
3203 }
3204 }
3205
3206 pub fn applyAttributes(self: *Block, string_table: *Node.StringTable, source: []const u8, code_page_lookup: *const CodePageLookup) void {
3207 Compiler.applyToMemoryFlags(&self.memory_flags, string_table.common_resource_attributes, source);
3208 var dummy_language: res.Language = undefined;
3209 Compiler.applyToOptionalStatements(&dummy_language, &self.version, &self.characteristics, string_table.optional_statements, source, code_page_lookup);
3210 }
3211
3212 fn trimToDoubleNUL(comptime T: type, str: []const T) []const T {
3213 var last_was_null = false;
3214 for (str, 0..) |c, i| {
3215 if (c == 0) {
3216 if (last_was_null) return str[0 .. i - 1];
3217 last_was_null = true;
3218 } else {
3219 last_was_null = false;
3220 }
3221 }
3222 return str;
3223 }
3224
3225 test "trimToDoubleNUL" {
3226 try std.testing.expectEqualStrings("a\x00b", trimToDoubleNUL(u8, "a\x00b"));
3227 try std.testing.expectEqualStrings("a", trimToDoubleNUL(u8, "a\x00\x00b"));
3228 }
3229
3230 pub fn writeResData(self: *Block, compiler: *Compiler, language: res.Language, block_id: u16, writer: anytype) !void {
3231 var data_buffer = std.ArrayList(u8).init(compiler.allocator);
3232 defer data_buffer.deinit();
3233 const data_writer = data_buffer.writer();
3234
3235 var i: u8 = 0;
3236 var string_i: u8 = 0;
3237 while (true) : (i += 1) {
3238 if (!self.set_indexes.isSet(i)) {
3239 try data_writer.writeInt(u16, 0, .little);
3240 if (i == 15) break else continue;
3241 }
3242
3243 const string_token = self.strings.items[string_i];
3244 const slice = string_token.slice(compiler.source);
3245 const column = string_token.calculateColumn(compiler.source, 8, null);
3246 const code_page = compiler.input_code_pages.getForToken(string_token);
3247 const bytes = SourceBytes{ .slice = slice, .code_page = code_page };
3248 const utf16_string = try literals.parseQuotedStringAsWideString(compiler.allocator, bytes, .{
3249 .start_column = column,
3250 .diagnostics = .{ .diagnostics = compiler.diagnostics, .token = string_token },
3251 });
3252 defer compiler.allocator.free(utf16_string);
3253
3254 const trimmed_string = trim: {
3255 // Two NUL characters in a row act as a terminator
3256 // Note: This is only the case for STRINGTABLE strings
3257 const trimmed = trimToDoubleNUL(u16, utf16_string);
3258 // We also want to trim any trailing NUL characters
3259 break :trim std.mem.trimRight(u16, trimmed, &[_]u16{0});
3260 };
3261
3262 // String literals are limited to maxInt(u15) codepoints, so these UTF-16 encoded
3263 // strings are limited to maxInt(u15) * 2 = 65,534 code units (since 2 is the
3264 // maximum number of UTF-16 code units per codepoint).
3265 // This leaves room for exactly one NUL terminator.
3266 var string_len_in_utf16_code_units: u16 = @intCast(trimmed_string.len);
3267 // If the option is set, then a NUL terminator is added unconditionally.
3268 // We already trimmed any trailing NULs, so we know it will be a new addition to the string.
3269 if (compiler.null_terminate_string_table_strings) string_len_in_utf16_code_units += 1;
3270 try data_writer.writeInt(u16, string_len_in_utf16_code_units, .little);
3271 try data_writer.writeAll(std.mem.sliceAsBytes(trimmed_string));
3272 if (compiler.null_terminate_string_table_strings) {
3273 try data_writer.writeInt(u16, 0, .little);
3274 }
3275
3276 if (i == 15) break;
3277 string_i += 1;
3278 }
3279
3280 // This intCast will never be able to fail due to the length constraints on string literals.
3281 //
3282 // - STRINGTABLE resource definitions can can only provide one string literal per index.
3283 // - STRINGTABLE strings are limited to maxInt(u16) UTF-16 code units (see 'string_len_in_utf16_code_units'
3284 // above), which means that the maximum number of bytes per string literal is
3285 // 2 * maxInt(u16) = 131,070 (since there are 2 bytes per UTF-16 code unit).
3286 // - Each Block/RT_STRING resource includes exactly 16 strings and each have a 2 byte
3287 // length field, so the maximum number of total bytes in a RT_STRING resource's data is
3288 // 16 * (131,070 + 2) = 2,097,152 which is well within the u32 max.
3289 //
3290 // Note: The string literal maximum length is enforced by the lexer.
3291 const data_size: u32 = @intCast(data_buffer.items.len);
3292
3293 const header = Compiler.ResourceHeader{
3294 .name_value = .{ .ordinal = block_id },
3295 .type_value = .{ .ordinal = @intFromEnum(res.RT.STRING) },
3296 .memory_flags = self.memory_flags,
3297 .language = language,
3298 .version = self.version,
3299 .characteristics = self.characteristics,
3300 .data_size = data_size,
3301 };
3302 // The only variable parts of the header are name and type, which in this case
3303 // we fully control and know are numbers, so they have a fixed size.
3304 try header.writeAssertNoOverflow(writer);
3305
3306 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
3307 try Compiler.writeResourceData(writer, data_fbs.reader(), data_size);
3308 }
3309 };
3310
3311 pub fn deinit(self: *StringTable, allocator: Allocator) void {
3312 var it = self.blocks.iterator();
3313 while (it.next()) |entry| {
3314 entry.value_ptr.strings.deinit(allocator);
3315 }
3316 self.blocks.deinit(allocator);
3317 }
3318
3319 const SetError = error{StringAlreadyDefined} || Allocator.Error;
3320
3321 pub fn set(
3322 self: *StringTable,
3323 allocator: Allocator,
3324 id: u16,
3325 string_token: Token,
3326 node: *Node,
3327 source: []const u8,
3328 code_page_lookup: *const CodePageLookup,
3329 version: u32,
3330 characteristics: u32,
3331 ) SetError!void {
3332 const block_id = (id / 16) + 1;
3333 const string_index: u8 = @intCast(id & 0xF);
3334
3335 var get_or_put_result = try self.blocks.getOrPut(allocator, block_id);
3336 if (!get_or_put_result.found_existing) {
3337 get_or_put_result.value_ptr.* = Block{ .version = version, .characteristics = characteristics };
3338 get_or_put_result.value_ptr.applyAttributes(node.cast(.string_table).?, source, code_page_lookup);
3339 } else {
3340 if (get_or_put_result.value_ptr.set_indexes.isSet(string_index)) {
3341 return error.StringAlreadyDefined;
3342 }
3343 }
3344
3345 var block = get_or_put_result.value_ptr;
3346 if (block.getInsertionIndex(string_index)) |insertion_index| {
3347 try block.strings.insert(allocator, insertion_index, string_token);
3348 } else {
3349 try block.strings.append(allocator, string_token);
3350 }
3351 block.set_indexes.set(string_index);
3352 }
3353
3354 pub fn get(self: *StringTable, id: u16) ?Token {
3355 const block_id = (id / 16) + 1;
3356 const string_index: u8 = @intCast(id & 0xF);
3357
3358 const block = self.blocks.getPtr(block_id) orelse return null;
3359 const token_index = block.getTokenIndex(string_index) orelse return null;
3360 return block.strings.items[token_index];
3361 }
3362
3363 pub fn dump(self: *StringTable) !void {
3364 var it = self.iterator();
3365 while (it.next()) |entry| {
3366 std.debug.print("block: {}\n", .{entry.key_ptr.*});
3367 entry.value_ptr.dump();
3368 }
3369 }
3370};
3371
3372test "StringTable" {
3373 const S = struct {
3374 fn makeDummyToken(id: usize) Token {
3375 return Token{
3376 .id = .invalid,
3377 .start = id,
3378 .end = id,
3379 .line_number = id,
3380 };
3381 }
3382 };
3383 const allocator = std.testing.allocator;
3384 var string_table = StringTable{};
3385 defer string_table.deinit(allocator);
3386
3387 var code_page_lookup = CodePageLookup.init(allocator, .windows1252);
3388 defer code_page_lookup.deinit();
3389
3390 var dummy_node = Node.StringTable{
3391 .type = S.makeDummyToken(0),
3392 .common_resource_attributes = &.{},
3393 .optional_statements = &.{},
3394 .begin_token = S.makeDummyToken(0),
3395 .strings = &.{},
3396 .end_token = S.makeDummyToken(0),
3397 };
3398
3399 // randomize an array of ids 0-99
3400 var ids = ids: {
3401 var buf: [100]u16 = undefined;
3402 var i: u16 = 0;
3403 while (i < buf.len) : (i += 1) {
3404 buf[i] = i;
3405 }
3406 break :ids buf;
3407 };
3408 var prng = std.rand.DefaultPrng.init(0);
3409 var random = prng.random();
3410 random.shuffle(u16, &ids);
3411
3412 // set each one in the randomized order
3413 for (ids) |id| {
3414 try string_table.set(allocator, id, S.makeDummyToken(id), &dummy_node.base, "", &code_page_lookup, 0, 0);
3415 }
3416
3417 // make sure each one exists and is the right value when gotten
3418 var id: u16 = 0;
3419 while (id < 100) : (id += 1) {
3420 const dummy = S.makeDummyToken(id);
3421 try std.testing.expectError(error.StringAlreadyDefined, string_table.set(allocator, id, dummy, &dummy_node.base, "", &code_page_lookup, 0, 0));
3422 try std.testing.expectEqual(dummy, string_table.get(id).?);
3423 }
3424
3425 // make sure non-existent string ids are not found
3426 try std.testing.expectEqual(@as(?Token, null), string_table.get(100));
3427}
lib/compiler/resinator/errors.zig created+1076
...@@ -0,0 +1,1076 @@
1const std = @import("std");
2const Token = @import("lex.zig").Token;
3const SourceMappings = @import("source_mapping.zig").SourceMappings;
4const utils = @import("utils.zig");
5const rc = @import("rc.zig");
6const res = @import("res.zig");
7const ico = @import("ico.zig");
8const bmp = @import("bmp.zig");
9const parse = @import("parse.zig");
10const lang = @import("lang.zig");
11const CodePage = @import("code_pages.zig").CodePage;
12const builtin = @import("builtin");
13const native_endian = builtin.cpu.arch.endian();
14
15pub const Diagnostics = struct {
16 errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
17 /// Append-only, cannot handle removing strings.
18 /// Expects to own all strings within the list.
19 strings: std.ArrayListUnmanaged([]const u8) = .{},
20 allocator: std.mem.Allocator,
21
22 pub fn init(allocator: std.mem.Allocator) Diagnostics {
23 return .{
24 .allocator = allocator,
25 };
26 }
27
28 pub fn deinit(self: *Diagnostics) void {
29 self.errors.deinit(self.allocator);
30 for (self.strings.items) |str| {
31 self.allocator.free(str);
32 }
33 self.strings.deinit(self.allocator);
34 }
35
36 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
37 try self.errors.append(self.allocator, error_details);
38 }
39
40 const SmallestStringIndexType = std.meta.Int(.unsigned, @min(
41 @bitSizeOf(ErrorDetails.FileOpenError.FilenameStringIndex),
42 @min(
43 @bitSizeOf(ErrorDetails.IconReadError.FilenameStringIndex),
44 @bitSizeOf(ErrorDetails.BitmapReadError.FilenameStringIndex),
45 ),
46 ));
47
48 /// Returns the index of the added string as the SmallestStringIndexType
49 /// in order to avoid needing to `@intCast` it at callsites of putString.
50 /// Instead, this function will error if the index would ever exceed the
51 /// smallest FilenameStringIndex of an ErrorDetails type.
52 pub fn putString(self: *Diagnostics, str: []const u8) !SmallestStringIndexType {
53 if (self.strings.items.len >= std.math.maxInt(SmallestStringIndexType)) {
54 return error.OutOfMemory; // ran out of string indexes
55 }
56 const dupe = try self.allocator.dupe(u8, str);
57 const index = self.strings.items.len;
58 try self.strings.append(self.allocator, dupe);
59 return @intCast(index);
60 }
61
62 pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.io.tty.Config, source_mappings: ?SourceMappings) void {
63 std.debug.getStderrMutex().lock();
64 defer std.debug.getStderrMutex().unlock();
65 const stderr = std.io.getStdErr().writer();
66 for (self.errors.items) |err_details| {
67 renderErrorMessage(self.allocator, stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return;
68 }
69 }
70
71 pub fn renderToStdErrDetectTTY(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void {
72 const tty_config = std.io.tty.detectConfig(std.io.getStdErr());
73 return self.renderToStdErr(cwd, source, tty_config, source_mappings);
74 }
75
76 pub fn contains(self: *const Diagnostics, err: ErrorDetails.Error) bool {
77 for (self.errors.items) |details| {
78 if (details.err == err) return true;
79 }
80 return false;
81 }
82
83 pub fn containsAny(self: *const Diagnostics, errors: []const ErrorDetails.Error) bool {
84 for (self.errors.items) |details| {
85 for (errors) |err| {
86 if (details.err == err) return true;
87 }
88 }
89 return false;
90 }
91};
92
93/// Contains enough context to append errors/warnings/notes etc
94pub const DiagnosticsContext = struct {
95 diagnostics: *Diagnostics,
96 token: Token,
97};
98
99pub const ErrorDetails = struct {
100 err: Error,
101 token: Token,
102 /// If non-null, should be before `token`. If null, `token` is assumed to be the start.
103 token_span_start: ?Token = null,
104 /// If non-null, should be after `token`. If null, `token` is assumed to be the end.
105 token_span_end: ?Token = null,
106 type: Type = .err,
107 print_source_line: bool = true,
108 extra: union {
109 none: void,
110 expected: Token.Id,
111 number: u32,
112 expected_types: ExpectedTypes,
113 resource: rc.Resource,
114 string_and_language: StringAndLanguage,
115 file_open_error: FileOpenError,
116 icon_read_error: IconReadError,
117 icon_dir: IconDirContext,
118 bmp_read_error: BitmapReadError,
119 accelerator_error: AcceleratorError,
120 statement_with_u16_param: StatementWithU16Param,
121 menu_or_class: enum { class, menu },
122 } = .{ .none = {} },
123
124 pub const Type = enum {
125 /// Fatal error, stops compilation
126 err,
127 /// Warning that does not affect compilation result
128 warning,
129 /// A note that typically provides further context for a warning/error
130 note,
131 /// An invisible diagnostic that is not printed to stderr but can
132 /// provide information useful when comparing the behavior of different
133 /// implementations. For example, a hint is emitted when a FONTDIR resource
134 /// was included in the .RES file which is significant because rc.exe
135 /// does something different than us, but ultimately it's not important
136 /// enough to be a warning/note.
137 hint,
138 };
139
140 comptime {
141 // all fields in the extra union should be 32 bits or less
142 for (std.meta.fields(std.meta.fieldInfo(ErrorDetails, .extra).type)) |field| {
143 std.debug.assert(@bitSizeOf(field.type) <= 32);
144 }
145 }
146
147 pub const StatementWithU16Param = enum(u32) {
148 fileversion,
149 productversion,
150 language,
151 };
152
153 pub const StringAndLanguage = packed struct(u32) {
154 id: u16,
155 language: res.Language,
156 };
157
158 pub const FileOpenError = packed struct(u32) {
159 err: FileOpenErrorEnum,
160 filename_string_index: FilenameStringIndex,
161
162 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum));
163 pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError);
164
165 pub fn enumFromError(err: std.fs.File.OpenError) FileOpenErrorEnum {
166 return switch (err) {
167 inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)),
168 };
169 }
170 };
171
172 pub const IconReadError = packed struct(u32) {
173 err: IconReadErrorEnum,
174 icon_type: enum(u1) { cursor, icon },
175 filename_string_index: FilenameStringIndex,
176
177 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(IconReadErrorEnum) - 1);
178 pub const IconReadErrorEnum = std.meta.FieldEnum(ico.ReadError);
179
180 pub fn enumFromError(err: ico.ReadError) IconReadErrorEnum {
181 return switch (err) {
182 inline else => |e| @field(ErrorDetails.IconReadError.IconReadErrorEnum, @errorName(e)),
183 };
184 }
185 };
186
187 pub const IconDirContext = packed struct(u32) {
188 icon_type: enum(u1) { cursor, icon },
189 icon_format: ico.ImageFormat,
190 index: u16,
191 bitmap_version: ico.BitmapHeader.Version = .unknown,
192 _: Padding = 0,
193
194 pub const Padding = std.meta.Int(.unsigned, 15 - @bitSizeOf(ico.BitmapHeader.Version) - @bitSizeOf(ico.ImageFormat));
195 };
196
197 pub const BitmapReadError = packed struct(u32) {
198 err: BitmapReadErrorEnum,
199 filename_string_index: FilenameStringIndex,
200
201 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(BitmapReadErrorEnum));
202 pub const BitmapReadErrorEnum = std.meta.FieldEnum(bmp.ReadError);
203
204 pub fn enumFromError(err: bmp.ReadError) BitmapReadErrorEnum {
205 return switch (err) {
206 inline else => |e| @field(ErrorDetails.BitmapReadError.BitmapReadErrorEnum, @errorName(e)),
207 };
208 }
209 };
210
211 pub const BitmapUnsupportedDIB = packed struct(u32) {
212 dib_version: ico.BitmapHeader.Version,
213 filename_string_index: FilenameStringIndex,
214
215 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(ico.BitmapHeader.Version));
216 };
217
218 pub const AcceleratorError = packed struct(u32) {
219 err: AcceleratorErrorEnum,
220 _: Padding = 0,
221
222 pub const Padding = std.meta.Int(.unsigned, 32 - @bitSizeOf(AcceleratorErrorEnum));
223 pub const AcceleratorErrorEnum = std.meta.FieldEnum(res.ParseAcceleratorKeyStringError);
224
225 pub fn enumFromError(err: res.ParseAcceleratorKeyStringError) AcceleratorErrorEnum {
226 return switch (err) {
227 inline else => |e| @field(ErrorDetails.AcceleratorError.AcceleratorErrorEnum, @errorName(e)),
228 };
229 }
230 };
231
232 pub const ExpectedTypes = packed struct(u32) {
233 number: bool = false,
234 number_expression: bool = false,
235 string_literal: bool = false,
236 accelerator_type_or_option: bool = false,
237 control_class: bool = false,
238 literal: bool = false,
239 // Note: This being 0 instead of undefined is arbitrary and something of a workaround,
240 // see https://github.com/ziglang/zig/issues/15395
241 _: u26 = 0,
242
243 pub const strings = std.ComptimeStringMap([]const u8, .{
244 .{ "number", "number" },
245 .{ "number_expression", "number expression" },
246 .{ "string_literal", "quoted string literal" },
247 .{ "accelerator_type_or_option", "accelerator type or option [ASCII, VIRTKEY, etc]" },
248 .{ "control_class", "control class [BUTTON, EDIT, etc]" },
249 .{ "literal", "unquoted literal" },
250 });
251
252 pub fn writeCommaSeparated(self: ExpectedTypes, writer: anytype) !void {
253 const struct_info = @typeInfo(ExpectedTypes).Struct;
254 const num_real_fields = struct_info.fields.len - 1;
255 const num_padding_bits = @bitSizeOf(ExpectedTypes) - num_real_fields;
256 const mask = std.math.maxInt(struct_info.backing_integer.?) >> num_padding_bits;
257 const relevant_bits_only = @as(struct_info.backing_integer.?, @bitCast(self)) & mask;
258 const num_set_bits = @popCount(relevant_bits_only);
259
260 var i: usize = 0;
261 inline for (struct_info.fields) |field_info| {
262 if (field_info.type != bool) continue;
263 if (i == num_set_bits) return;
264 if (@field(self, field_info.name)) {
265 try writer.writeAll(strings.get(field_info.name).?);
266 i += 1;
267 if (num_set_bits > 2 and i != num_set_bits) {
268 try writer.writeAll(", ");
269 } else if (i != num_set_bits) {
270 try writer.writeByte(' ');
271 }
272 if (num_set_bits > 1 and i == num_set_bits - 1) {
273 try writer.writeAll("or ");
274 }
275 }
276 }
277 }
278 };
279
280 pub const Error = enum {
281 // Lexer
282 unfinished_string_literal,
283 string_literal_too_long,
284 invalid_number_with_exponent,
285 invalid_digit_character_in_number_literal,
286 illegal_byte,
287 illegal_byte_outside_string_literals,
288 illegal_codepoint_outside_string_literals,
289 illegal_byte_order_mark,
290 illegal_private_use_character,
291 found_c_style_escaped_quote,
292 code_page_pragma_missing_left_paren,
293 code_page_pragma_missing_right_paren,
294 code_page_pragma_invalid_code_page,
295 code_page_pragma_not_integer,
296 code_page_pragma_overflow,
297 code_page_pragma_unsupported_code_page,
298
299 // Parser
300 unfinished_raw_data_block,
301 unfinished_string_table_block,
302 /// `expected` is populated.
303 expected_token,
304 /// `expected_types` is populated
305 expected_something_else,
306 /// `resource` is populated
307 resource_type_cant_use_raw_data,
308 /// `resource` is populated
309 id_must_be_ordinal,
310 /// `resource` is populated
311 name_or_id_not_allowed,
312 string_resource_as_numeric_type,
313 ascii_character_not_equivalent_to_virtual_key_code,
314 empty_menu_not_allowed,
315 rc_would_miscompile_version_value_padding,
316 rc_would_miscompile_version_value_byte_count,
317 code_page_pragma_in_included_file,
318 nested_resource_level_exceeds_max,
319 too_many_dialog_controls_or_toolbar_buttons,
320 nested_expression_level_exceeds_max,
321 close_paren_expression,
322 unary_plus_expression,
323 rc_could_miscompile_control_params,
324
325 // Compiler
326 /// `string_and_language` is populated
327 string_already_defined,
328 font_id_already_defined,
329 /// `file_open_error` is populated
330 file_open_error,
331 /// `accelerator_error` is populated
332 invalid_accelerator_key,
333 accelerator_type_required,
334 rc_would_miscompile_control_padding,
335 rc_would_miscompile_control_class_ordinal,
336 /// `icon_dir` is populated
337 rc_would_error_on_icon_dir,
338 /// `icon_dir` is populated
339 format_not_supported_in_icon_dir,
340 /// `resource` is populated and contains the expected type
341 icon_dir_and_resource_type_mismatch,
342 /// `icon_read_error` is populated
343 icon_read_error,
344 /// `icon_dir` is populated
345 rc_would_error_on_bitmap_version,
346 /// `icon_dir` is populated
347 max_icon_ids_exhausted,
348 /// `bmp_read_error` is populated
349 bmp_read_error,
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 ignored bytes.
352 bmp_ignored_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 missing bytes.
355 bmp_missing_palette_bytes,
356 /// `number` is populated and contains a string index for which the string contains
357 /// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.
358 rc_would_miscompile_bmp_palette_padding,
359 /// `number` is populated and contains a string index for which the string contains
360 /// the bytes of two `u64`s (native endian). The first contains the number of missing
361 /// palette bytes and the second contains the max number of missing palette bytes.
362 /// If type is `.note`, then `extra` is `none`.
363 bmp_too_many_missing_palette_bytes,
364 resource_header_size_exceeds_max,
365 resource_data_size_exceeds_max,
366 control_extra_data_size_exceeds_max,
367 version_node_size_exceeds_max,
368 fontdir_size_exceeds_max,
369 /// `number` is populated and contains a string index for the filename
370 number_expression_as_filename,
371 /// `number` is populated and contains the control ID that is a duplicate
372 control_id_already_defined,
373 /// `number` is populated and contains the disallowed codepoint
374 invalid_filename,
375 /// `statement_with_u16_param` is populated
376 rc_would_error_u16_with_l_suffix,
377 result_contains_fontdir,
378 /// `number` is populated and contains the ordinal value that the id would be miscompiled to
379 rc_would_miscompile_dialog_menu_id,
380 /// `number` is populated and contains the ordinal value that the value would be miscompiled to
381 rc_would_miscompile_dialog_class,
382 /// `menu_or_class` is populated and contains the type of the parameter statement
383 rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
384 rc_would_miscompile_dialog_menu_id_starts_with_digit,
385 dialog_menu_id_was_uppercased,
386 /// `menu_or_class` is populated and contains the type of the parameter statement
387 duplicate_menu_or_class_skipped,
388 invalid_digit_character_in_ordinal,
389
390 // Literals
391 /// `number` is populated
392 rc_would_miscompile_codepoint_byte_swap,
393 /// `number` is populated
394 rc_would_miscompile_codepoint_skip,
395 tab_converted_to_spaces,
396
397 // General (used in various places)
398 /// `number` is populated and contains the value that the ordinal would have in the Win32 RC compiler implementation
399 win32_non_ascii_ordinal,
400
401 // Initialization
402 /// `file_open_error` is populated, but `filename_string_index` is not
403 failed_to_open_cwd,
404 };
405
406 pub fn render(self: ErrorDetails, writer: anytype, source: []const u8, strings: []const []const u8) !void {
407 switch (self.err) {
408 .unfinished_string_literal => {
409 return writer.print("unfinished string literal at '{s}', expected closing '\"'", .{self.token.nameForErrorDisplay(source)});
410 },
411 .string_literal_too_long => {
412 return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number});
413 },
414 .invalid_number_with_exponent => {
415 return writer.print("base 10 number literal with exponent is not allowed: {s}", .{self.token.slice(source)});
416 },
417 .invalid_digit_character_in_number_literal => switch (self.type) {
418 .err, .warning => return writer.writeAll("non-ASCII digit characters are not allowed in number literals"),
419 .note => return writer.writeAll("the Win32 RC compiler allows non-ASCII digit characters, but will miscompile them"),
420 .hint => return,
421 },
422 .illegal_byte => {
423 return writer.print("character '{s}' is not allowed", .{std.fmt.fmtSliceEscapeUpper(self.token.slice(source))});
424 },
425 .illegal_byte_outside_string_literals => {
426 return writer.print("character '{s}' is not allowed outside of string literals", .{std.fmt.fmtSliceEscapeUpper(self.token.slice(source))});
427 },
428 .illegal_codepoint_outside_string_literals => {
429 // This is somewhat hacky, but we know that:
430 // - This error is only possible with codepoints outside of the Windows-1252 character range
431 // - So, the only supported code page that could generate this error is UTF-8
432 // Therefore, we just assume the token bytes are UTF-8 and decode them to get the illegal
433 // codepoint.
434 //
435 // FIXME: Support other code pages if they become relevant
436 const bytes = self.token.slice(source);
437 const codepoint = std.unicode.utf8Decode(bytes) catch unreachable;
438 return writer.print("codepoint <U+{X:0>4}> is not allowed outside of string literals", .{codepoint});
439 },
440 .illegal_byte_order_mark => {
441 return writer.writeAll("byte order mark <U+FEFF> is not allowed");
442 },
443 .illegal_private_use_character => {
444 return writer.writeAll("private use character <U+E000> is not allowed");
445 },
446 .found_c_style_escaped_quote => {
447 return writer.writeAll("escaping quotes with \\\" is not allowed (use \"\" instead)");
448 },
449 .code_page_pragma_missing_left_paren => {
450 return writer.writeAll("expected left parenthesis after 'code_page' in #pragma code_page");
451 },
452 .code_page_pragma_missing_right_paren => {
453 return writer.writeAll("expected right parenthesis after '<number>' in #pragma code_page");
454 },
455 .code_page_pragma_invalid_code_page => {
456 return writer.writeAll("invalid or unknown code page in #pragma code_page");
457 },
458 .code_page_pragma_not_integer => {
459 return writer.writeAll("code page is not a valid integer in #pragma code_page");
460 },
461 .code_page_pragma_overflow => {
462 return writer.writeAll("code page too large in #pragma code_page");
463 },
464 .code_page_pragma_unsupported_code_page => {
465 // We know that the token slice is a well-formed #pragma code_page(N), so
466 // we can skip to the first ( and then get the number that follows
467 const token_slice = self.token.slice(source);
468 var number_start = std.mem.indexOfScalar(u8, token_slice, '(').? + 1;
469 while (std.ascii.isWhitespace(token_slice[number_start])) {
470 number_start += 1;
471 }
472 var number_slice = token_slice[number_start..number_start];
473 while (std.ascii.isDigit(token_slice[number_start + number_slice.len])) {
474 number_slice.len += 1;
475 }
476 const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable;
477 const code_page = CodePage.getByIdentifier(number) catch unreachable;
478 // TODO: Improve or maybe add a note making it more clear that the code page
479 // is valid and that the code page is unsupported purely due to a limitation
480 // in this compiler.
481 return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number });
482 },
483 .unfinished_raw_data_block => {
484 return writer.print("unfinished raw data block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});
485 },
486 .unfinished_string_table_block => {
487 return writer.print("unfinished STRINGTABLE block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});
488 },
489 .expected_token => {
490 return writer.print("expected '{s}', got '{s}'", .{ self.extra.expected.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });
491 },
492 .expected_something_else => {
493 try writer.writeAll("expected ");
494 try self.extra.expected_types.writeCommaSeparated(writer);
495 return writer.print("; got '{s}'", .{self.token.nameForErrorDisplay(source)});
496 },
497 .resource_type_cant_use_raw_data => switch (self.type) {
498 .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() }),
499 .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)}),
500 .hint => return,
501 },
502 .id_must_be_ordinal => {
503 try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{s}'", .{ self.extra.resource.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });
504 },
505 .name_or_id_not_allowed => {
506 try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()});
507 },
508 .string_resource_as_numeric_type => switch (self.type) {
509 .err, .warning => try writer.writeAll("the number 6 (RT_STRING) cannot be used as a resource type"),
510 .note => try writer.writeAll("using RT_STRING directly likely results in an invalid .res file, use a STRINGTABLE instead"),
511 .hint => return,
512 },
513 .ascii_character_not_equivalent_to_virtual_key_code => {
514 // TODO: Better wording? This is what the Win32 RC compiler emits.
515 // This occurs when VIRTKEY and a control code is specified ("^c", etc)
516 try writer.writeAll("ASCII character not equivalent to virtual key code");
517 },
518 .empty_menu_not_allowed => {
519 try writer.print("empty menu of type '{s}' not allowed", .{self.token.nameForErrorDisplay(source)});
520 },
521 .rc_would_miscompile_version_value_padding => switch (self.type) {
522 .err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}),
523 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma between the key and the quoted string", .{}),
524 .hint => return,
525 },
526 .rc_would_miscompile_version_value_byte_count => switch (self.type) {
527 .err, .warning => return writer.print("the byte count of this value would be miscompiled by the Win32 RC compiler", .{}),
528 .note => return writer.print("to avoid the potential miscompilation, do not mix numbers and strings within a value", .{}),
529 .hint => return,
530 },
531 .code_page_pragma_in_included_file => {
532 try writer.print("#pragma code_page is not supported in an included resource file", .{});
533 },
534 .nested_resource_level_exceeds_max => switch (self.type) {
535 .err, .warning => {
536 const max = switch (self.extra.resource) {
537 .versioninfo => parse.max_nested_version_level,
538 .menu, .menuex => parse.max_nested_menu_level,
539 else => unreachable,
540 };
541 return writer.print("{s} contains too many nested children (max is {})", .{ self.extra.resource.nameForErrorDisplay(), max });
542 },
543 .note => return writer.print("max {s} nesting level exceeded here", .{self.extra.resource.nameForErrorDisplay()}),
544 .hint => return,
545 },
546 .too_many_dialog_controls_or_toolbar_buttons => switch (self.type) {
547 .err, .warning => return writer.print("{s} contains too many {s} (max is {})", .{ self.extra.resource.nameForErrorDisplay(), switch (self.extra.resource) {
548 .toolbar => "buttons",
549 else => "controls",
550 }, std.math.maxInt(u16) }),
551 .note => return writer.print("maximum number of {s} exceeded here", .{switch (self.extra.resource) {
552 .toolbar => "buttons",
553 else => "controls",
554 }}),
555 .hint => return,
556 },
557 .nested_expression_level_exceeds_max => switch (self.type) {
558 .err, .warning => return writer.print("expression contains too many syntax levels (max is {})", .{parse.max_nested_expression_level}),
559 .note => return writer.print("maximum expression level exceeded here", .{}),
560 .hint => return,
561 },
562 .close_paren_expression => {
563 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");
564 },
565 .unary_plus_expression => {
566 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 +");
567 },
568 .rc_could_miscompile_control_params => switch (self.type) {
569 .err, .warning => return writer.print("this token could be erroneously skipped over by the Win32 RC compiler", .{}),
570 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}),
571 .hint => return,
572 },
573 .string_already_defined => switch (self.type) {
574 .err, .warning => {
575 const language_id = self.extra.string_and_language.language.asInt();
576 const language_name = language_name: {
577 if (std.meta.intToEnum(lang.LanguageId, language_id)) |lang_enum_val| {
578 break :language_name @tagName(lang_enum_val);
579 } else |_| {}
580 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
581 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
582 }
583 break :language_name "<UNKNOWN>";
584 };
585 return writer.print("string with id {d} (0x{X}) already defined for language {s} (0x{X})", .{ self.extra.string_and_language.id, self.extra.string_and_language.id, language_name, language_id });
586 },
587 .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 }),
588 .hint => return,
589 },
590 .font_id_already_defined => switch (self.type) {
591 .err => return writer.print("font with id {d} already defined", .{self.extra.number}),
592 .warning => return writer.print("skipped duplicate font with id {d}", .{self.extra.number}),
593 .note => return writer.print("previous definition of font with id {d} here", .{self.extra.number}),
594 .hint => return,
595 },
596 .file_open_error => {
597 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) });
598 },
599 .invalid_accelerator_key => {
600 try writer.print("invalid accelerator key '{s}': {s}", .{ self.token.nameForErrorDisplay(source), @tagName(self.extra.accelerator_error.err) });
601 },
602 .accelerator_type_required => {
603 try writer.print("accelerator type [ASCII or VIRTKEY] required when key is an integer", .{});
604 },
605 .rc_would_miscompile_control_padding => switch (self.type) {
606 .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)", .{}),
607 .note => return writer.print("to avoid the potential miscompilation, consider removing any 'control data' blocks from the controls in this dialog", .{}),
608 .hint => return,
609 },
610 .rc_would_miscompile_control_class_ordinal => switch (self.type) {
611 .err, .warning => return writer.print("the control class of this CONTROL would be miscompiled by the Win32 RC compiler", .{}),
612 .note => return writer.print("to avoid the potential miscompilation, consider specifying the control class using a string (BUTTON, EDIT, etc) instead of a number", .{}),
613 .hint => return,
614 },
615 .rc_would_error_on_icon_dir => switch (self.type) {
616 .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) }),
617 .note => {
618 // The only note supported is one specific to exactly this combination
619 if (!(self.extra.icon_dir.icon_type == .icon and self.extra.icon_dir.icon_format == .riff)) unreachable;
620 try writer.print("animated RIFF icons within resource groups may not be well supported, consider using an animated icon file (.ani) instead", .{});
621 },
622 .hint => return,
623 },
624 .format_not_supported_in_icon_dir => {
625 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) });
626 },
627 .icon_dir_and_resource_type_mismatch => {
628 const unexpected_type: rc.Resource = if (self.extra.resource == .icon) .cursor else .icon;
629 // TODO: Better wording
630 try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() });
631 },
632 .icon_read_error => {
633 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) });
634 },
635 .rc_would_error_on_bitmap_version => switch (self.type) {
636 .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}'", .{
637 self.extra.icon_dir.index,
638 @tagName(self.extra.icon_dir.icon_type),
639 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
640 ico.BitmapHeader.Version.@"nt3.1".nameForErrorDisplay(),
641 }),
642 .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", .{
643 self.extra.icon_dir.index,
644 @tagName(self.extra.icon_dir.icon_type),
645 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
646 }),
647 .note => unreachable,
648 .hint => return,
649 },
650 .max_icon_ids_exhausted => switch (self.type) {
651 .err, .warning => try writer.print("maximum global icon/cursor ids exhausted (max is {})", .{std.math.maxInt(u16) - 1}),
652 .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) }),
653 .hint => return,
654 },
655 .bmp_read_error => {
656 try writer.print("invalid bitmap file '{s}': {s}", .{ strings[self.extra.bmp_read_error.filename_string_index], @tagName(self.extra.bmp_read_error.err) });
657 },
658 .bmp_ignored_palette_bytes => {
659 const bytes = strings[self.extra.number];
660 const ignored_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
661 try writer.print("bitmap has {d} extra bytes preceding the pixel data which will be ignored", .{ignored_bytes});
662 },
663 .bmp_missing_palette_bytes => {
664 const bytes = strings[self.extra.number];
665 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
666 try writer.print("bitmap has {d} missing color palette bytes which will be padded with zeroes", .{missing_bytes});
667 },
668 .rc_would_miscompile_bmp_palette_padding => {
669 const bytes = strings[self.extra.number];
670 const miscompiled_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
671 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});
672 },
673 .bmp_too_many_missing_palette_bytes => switch (self.type) {
674 .err, .warning => {
675 const bytes = strings[self.extra.number];
676 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
677 const max_missing_bytes = std.mem.readInt(u64, bytes[8..16], native_endian);
678 try writer.print("bitmap has {} missing color palette bytes which exceeds the maximum of {}", .{ missing_bytes, max_missing_bytes });
679 },
680 // TODO: command line option
681 .note => try writer.writeAll("the maximum number of missing color palette bytes is configurable via <<TODO command line option>>"),
682 .hint => return,
683 },
684 .resource_header_size_exceeds_max => {
685 try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)});
686 },
687 .resource_data_size_exceeds_max => switch (self.type) {
688 .err, .warning => return writer.print("resource's data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
689 .note => return writer.print("maximum data length exceeded here", .{}),
690 .hint => return,
691 },
692 .control_extra_data_size_exceeds_max => switch (self.type) {
693 .err, .warning => try writer.print("control data length exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
694 .note => return writer.print("maximum control data length exceeded here", .{}),
695 .hint => return,
696 },
697 .version_node_size_exceeds_max => switch (self.type) {
698 .err, .warning => return writer.print("version node tree size exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
699 .note => return writer.print("maximum tree size exceeded while writing this child", .{}),
700 .hint => return,
701 },
702 .fontdir_size_exceeds_max => switch (self.type) {
703 .err, .warning => return writer.print("FONTDIR data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
704 .note => return writer.writeAll("this is likely due to the size of the combined lengths of the device/face names of all FONT resources"),
705 .hint => return,
706 },
707 .number_expression_as_filename => switch (self.type) {
708 .err, .warning => return writer.writeAll("filename cannot be specified using a number expression, consider using a quoted string instead"),
709 .note => return writer.print("the Win32 RC compiler would evaluate this number expression as the filename '{s}'", .{strings[self.extra.number]}),
710 .hint => return,
711 },
712 .control_id_already_defined => switch (self.type) {
713 .err, .warning => return writer.print("control with id {d} already defined for this dialog", .{self.extra.number}),
714 .note => return writer.print("previous definition of control with id {d} here", .{self.extra.number}),
715 .hint => return,
716 },
717 .invalid_filename => {
718 const disallowed_codepoint = self.extra.number;
719 if (disallowed_codepoint < 128 and std.ascii.isPrint(@intCast(disallowed_codepoint))) {
720 try writer.print("evaluated filename contains a disallowed character: '{c}'", .{@as(u8, @intCast(disallowed_codepoint))});
721 } else {
722 try writer.print("evaluated filename contains a disallowed codepoint: <U+{X:0>4}>", .{disallowed_codepoint});
723 }
724 },
725 .rc_would_error_u16_with_l_suffix => switch (self.type) {
726 .err, .warning => return writer.print("this {s} parameter would be an error in the Win32 RC compiler", .{@tagName(self.extra.statement_with_u16_param)}),
727 .note => return writer.writeAll("to avoid the error, remove any L suffixes from numbers within the parameter"),
728 .hint => return,
729 },
730 .result_contains_fontdir => return,
731 .rc_would_miscompile_dialog_menu_id => switch (self.type) {
732 .err, .warning => return writer.print("the id of this menu would be miscompiled by the Win32 RC compiler", .{}),
733 .note => return writer.print("the Win32 RC compiler would evaluate the id as the ordinal/number value {d}", .{self.extra.number}),
734 .hint => return,
735 },
736 .rc_would_miscompile_dialog_class => switch (self.type) {
737 .err, .warning => return writer.print("this class would be miscompiled by the Win32 RC compiler", .{}),
738 .note => return writer.print("the Win32 RC compiler would evaluate it as the ordinal/number value {d}", .{self.extra.number}),
739 .hint => return,
740 },
741 .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal => switch (self.type) {
742 .err, .warning => return,
743 .note => return writer.print("to avoid the potential miscompilation, only specify one {s} per dialog resource", .{@tagName(self.extra.menu_or_class)}),
744 .hint => return,
745 },
746 .rc_would_miscompile_dialog_menu_id_starts_with_digit => switch (self.type) {
747 .err, .warning => return,
748 .note => return writer.writeAll("to avoid the potential miscompilation, the first character of the id should not be a digit"),
749 .hint => return,
750 },
751 .dialog_menu_id_was_uppercased => return,
752 .duplicate_menu_or_class_skipped => {
753 return writer.print("this {s} was ignored; when multiple {s} statements are specified, only the last takes precedence", .{
754 @tagName(self.extra.menu_or_class),
755 @tagName(self.extra.menu_or_class),
756 });
757 },
758 .invalid_digit_character_in_ordinal => {
759 return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values");
760 },
761 .rc_would_miscompile_codepoint_byte_swap => switch (self.type) {
762 .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}),
763 .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}),
764 .hint => return,
765 },
766 .rc_would_miscompile_codepoint_skip => switch (self.type) {
767 .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}),
768 .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}),
769 .hint => return,
770 },
771 .tab_converted_to_spaces => switch (self.type) {
772 .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)"),
773 .note => return writer.writeAll("to include the tab character itself in a string, the escape sequence \\t should be used"),
774 .hint => return,
775 },
776 .win32_non_ascii_ordinal => switch (self.type) {
777 .err, .warning => unreachable,
778 .note => return writer.print("the Win32 RC compiler would accept this as an ordinal but its value would be {}", .{self.extra.number}),
779 .hint => return,
780 },
781 .failed_to_open_cwd => {
782 try writer.print("failed to open CWD for compilation: {s}", .{@tagName(self.extra.file_open_error.err)});
783 },
784 }
785 }
786
787 pub const VisualTokenInfo = struct {
788 before_len: usize,
789 point_offset: usize,
790 after_len: usize,
791 };
792
793 pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize) VisualTokenInfo {
794 // Note: A perfect solution here would involve full grapheme cluster
795 // awareness, but oh well. This will give incorrect offsets
796 // if there are any multibyte codepoints within the relevant span,
797 // and even more inflated for grapheme clusters.
798 //
799 // We mitigate this slightly when we know we'll be pointing at
800 // something that displays as 1 character.
801 return switch (self.err) {
802 // These can technically be more than 1 byte depending on encoding,
803 // but they always refer to one visual character/grapheme.
804 .illegal_byte,
805 .illegal_byte_outside_string_literals,
806 .illegal_codepoint_outside_string_literals,
807 .illegal_byte_order_mark,
808 .illegal_private_use_character,
809 => .{
810 .before_len = 0,
811 .point_offset = self.token.start - source_line_start,
812 .after_len = 0,
813 },
814 else => .{
815 .before_len = before: {
816 const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start);
817 break :before self.token.start - start;
818 },
819 .point_offset = self.token.start - source_line_start,
820 .after_len = after: {
821 const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end);
822 // end may be less than start when pointing to EOF
823 if (end <= self.token.start) break :after 0;
824 break :after end - self.token.start - 1;
825 },
826 },
827 };
828 }
829};
830
831pub 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 {
832 if (err_details.type == .hint) return;
833
834 const source_line_start = err_details.token.getLineStartForErrorDisplay(source);
835 // Treat tab stops as 1 column wide for error display purposes,
836 // and add one to get a 1-based column
837 const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1;
838
839 const corresponding_span: ?SourceMappings.CorrespondingSpan = if (source_mappings) |mappings|
840 mappings.getCorrespondingSpan(err_details.token.line_number)
841 else
842 null;
843 const corresponding_file: ?[]const u8 = if (source_mappings != null and corresponding_span != null)
844 source_mappings.?.files.get(corresponding_span.?.filename_offset)
845 else
846 null;
847
848 const err_line = if (corresponding_span) |span| span.start_line else err_details.token.line_number;
849
850 try tty_config.setColor(writer, .bold);
851 if (corresponding_file) |file| {
852 try writer.writeAll(file);
853 } else {
854 try tty_config.setColor(writer, .dim);
855 try writer.writeAll("<after preprocessor>");
856 try tty_config.setColor(writer, .reset);
857 try tty_config.setColor(writer, .bold);
858 }
859 try writer.print(":{d}:{d}: ", .{ err_line, column });
860 switch (err_details.type) {
861 .err => {
862 try tty_config.setColor(writer, .red);
863 try writer.writeAll("error: ");
864 },
865 .warning => {
866 try tty_config.setColor(writer, .yellow);
867 try writer.writeAll("warning: ");
868 },
869 .note => {
870 try tty_config.setColor(writer, .cyan);
871 try writer.writeAll("note: ");
872 },
873 .hint => unreachable,
874 }
875 try tty_config.setColor(writer, .reset);
876 try tty_config.setColor(writer, .bold);
877 try err_details.render(writer, source, strings);
878 try writer.writeByte('\n');
879 try tty_config.setColor(writer, .reset);
880
881 if (!err_details.print_source_line) {
882 try writer.writeByte('\n');
883 return;
884 }
885
886 const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);
887 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
888
889 // Need this to determine if the 'line originated from' note is worth printing
890 var source_line_for_display_buf = try std.ArrayList(u8).initCapacity(allocator, source_line.len);
891 defer source_line_for_display_buf.deinit();
892 try writeSourceSlice(source_line_for_display_buf.writer(), source_line);
893
894 // TODO: General handling of long lines, not tied to this specific error
895 if (err_details.err == .string_literal_too_long) {
896 const before_slice = source_line[0..@min(source_line.len, visual_info.point_offset + 16)];
897 try writeSourceSlice(writer, before_slice);
898 try tty_config.setColor(writer, .dim);
899 try writer.writeAll("<...truncated...>");
900 try tty_config.setColor(writer, .reset);
901 } else {
902 try writer.writeAll(source_line_for_display_buf.items);
903 }
904 try writer.writeByte('\n');
905
906 try tty_config.setColor(writer, .green);
907 const num_spaces = visual_info.point_offset - visual_info.before_len;
908 try writer.writeByteNTimes(' ', num_spaces);
909 try writer.writeByteNTimes('~', visual_info.before_len);
910 try writer.writeByte('^');
911 if (visual_info.after_len > 0) {
912 var num_squiggles = visual_info.after_len;
913 if (err_details.err == .string_literal_too_long) {
914 num_squiggles = @min(num_squiggles, 15);
915 }
916 try writer.writeByteNTimes('~', num_squiggles);
917 }
918 try writer.writeByte('\n');
919 try tty_config.setColor(writer, .reset);
920
921 if (corresponding_span != null and corresponding_file != null) {
922 var corresponding_lines = try CorrespondingLines.init(allocator, cwd, err_details, source_line_for_display_buf.items, corresponding_span.?, corresponding_file.?);
923 defer corresponding_lines.deinit(allocator);
924
925 if (!corresponding_lines.worth_printing_note) return;
926
927 try tty_config.setColor(writer, .bold);
928 if (corresponding_file) |file| {
929 try writer.writeAll(file);
930 } else {
931 try tty_config.setColor(writer, .dim);
932 try writer.writeAll("<after preprocessor>");
933 try tty_config.setColor(writer, .reset);
934 try tty_config.setColor(writer, .bold);
935 }
936 try writer.print(":{d}:{d}: ", .{ err_line, column });
937 try tty_config.setColor(writer, .cyan);
938 try writer.writeAll("note: ");
939 try tty_config.setColor(writer, .reset);
940 try tty_config.setColor(writer, .bold);
941 try writer.writeAll("this line originated from line");
942 if (corresponding_span.?.start_line != corresponding_span.?.end_line) {
943 try writer.print("s {}-{}", .{ corresponding_span.?.start_line, corresponding_span.?.end_line });
944 } else {
945 try writer.print(" {}", .{corresponding_span.?.start_line});
946 }
947 try writer.print(" of file '{s}'\n", .{corresponding_file.?});
948 try tty_config.setColor(writer, .reset);
949
950 if (!corresponding_lines.worth_printing_lines) return;
951
952 if (corresponding_lines.lines_is_error_message) {
953 try tty_config.setColor(writer, .red);
954 try writer.writeAll(" | ");
955 try tty_config.setColor(writer, .reset);
956 try tty_config.setColor(writer, .dim);
957 try writer.writeAll(corresponding_lines.lines.items);
958 try tty_config.setColor(writer, .reset);
959 try writer.writeAll("\n\n");
960 return;
961 }
962
963 try writer.writeAll(corresponding_lines.lines.items);
964 try writer.writeAll("\n\n");
965 }
966}
967
968const CorrespondingLines = struct {
969 worth_printing_note: bool = true,
970 worth_printing_lines: bool = true,
971 lines: std.ArrayListUnmanaged(u8) = .{},
972 lines_is_error_message: bool = false,
973
974 pub fn init(allocator: std.mem.Allocator, cwd: std.fs.Dir, err_details: ErrorDetails, lines_for_comparison: []const u8, corresponding_span: SourceMappings.CorrespondingSpan, corresponding_file: []const u8) !CorrespondingLines {
975 var corresponding_lines = CorrespondingLines{};
976
977 // We don't do line comparison for this error, so don't print the note if the line
978 // number is different
979 if (err_details.err == .string_literal_too_long and err_details.token.line_number == corresponding_span.start_line) {
980 corresponding_lines.worth_printing_note = false;
981 return corresponding_lines;
982 }
983
984 // Don't print the originating line for this error, we know it's really long
985 if (err_details.err == .string_literal_too_long) {
986 corresponding_lines.worth_printing_lines = false;
987 return corresponding_lines;
988 }
989
990 var writer = corresponding_lines.lines.writer(allocator);
991 if (utils.openFileNotDir(cwd, corresponding_file, .{})) |file| {
992 defer file.close();
993 var buffered_reader = std.io.bufferedReader(file.reader());
994 writeLinesFromStream(writer, buffered_reader.reader(), corresponding_span.start_line, corresponding_span.end_line) catch |err| switch (err) {
995 error.LinesNotFound => {
996 corresponding_lines.lines.clearRetainingCapacity();
997 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});
998 corresponding_lines.lines_is_error_message = true;
999 return corresponding_lines;
1000 },
1001 else => |e| return e,
1002 };
1003 } else |err| {
1004 corresponding_lines.lines.clearRetainingCapacity();
1005 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});
1006 corresponding_lines.lines_is_error_message = true;
1007 return corresponding_lines;
1008 }
1009
1010 // If the lines are the same as they were before preprocessing, skip printing the note entirely
1011 if (std.mem.eql(u8, lines_for_comparison, corresponding_lines.lines.items)) {
1012 corresponding_lines.worth_printing_note = false;
1013 }
1014 return corresponding_lines;
1015 }
1016
1017 pub fn deinit(self: *CorrespondingLines, allocator: std.mem.Allocator) void {
1018 self.lines.deinit(allocator);
1019 }
1020};
1021
1022fn writeSourceSlice(writer: anytype, slice: []const u8) !void {
1023 for (slice) |c| try writeSourceByte(writer, c);
1024}
1025
1026inline fn writeSourceByte(writer: anytype, byte: u8) !void {
1027 switch (byte) {
1028 '\x00'...'\x08', '\x0E'...'\x1F', '\x7F' => try writer.writeAll("�"),
1029 // \r is seemingly ignored by the RC compiler so skipping it when printing source lines
1030 // could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up
1031 // in the console as DATA but the compiler reads it as RCDATA)
1032 //
1033 // NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r
1034 // characters get converted to \n, but may become relevant if another
1035 // preprocessor is used instead.
1036 '\r' => {},
1037 '\t', '\x0B', '\x0C' => try writer.writeByte(' '),
1038 else => try writer.writeByte(byte),
1039 }
1040}
1041
1042pub fn writeLinesFromStream(writer: anytype, input: anytype, start_line: usize, end_line: usize) !void {
1043 var line_num: usize = 1;
1044 var last_byte: u8 = 0;
1045 while (try readByteOrEof(input)) |byte| {
1046 switch (byte) {
1047 '\n', '\r' => {
1048 if (!utils.isLineEndingPair(last_byte, byte)) {
1049 if (line_num == end_line) return;
1050 if (line_num >= start_line) try writeSourceByte(writer, byte);
1051 line_num += 1;
1052 } else {
1053 // reset last_byte to a non-line ending so that
1054 // consecutive CRLF pairs don't get treated as one
1055 // long line ending 'pair'
1056 last_byte = 0;
1057 continue;
1058 }
1059 },
1060 else => {
1061 if (line_num >= start_line) try writeSourceByte(writer, byte);
1062 },
1063 }
1064 last_byte = byte;
1065 }
1066 if (line_num != end_line) {
1067 return error.LinesNotFound;
1068 }
1069}
1070
1071pub fn readByteOrEof(reader: anytype) !?u8 {
1072 return reader.readByte() catch |err| switch (err) {
1073 error.EndOfStream => return null,
1074 else => |e| return e,
1075 };
1076}
lib/compiler/resinator/ico.zig created+312
...@@ -0,0 +1,312 @@
1//! https://devblogs.microsoft.com/oldnewthing/20120720-00/?p=7083
2//! https://learn.microsoft.com/en-us/previous-versions/ms997538(v=msdn.10)
3//! https://learn.microsoft.com/en-us/windows/win32/menurc/newheader
4//! https://learn.microsoft.com/en-us/windows/win32/menurc/resdir
5//! https://learn.microsoft.com/en-us/windows/win32/menurc/localheader
6
7const std = @import("std");
8const builtin = @import("builtin");
9const native_endian = builtin.cpu.arch.endian();
10
11pub const ReadError = std.mem.Allocator.Error || error{ InvalidHeader, InvalidImageType, ImpossibleDataSize, UnexpectedEOF, ReadError };
12
13pub fn read(allocator: std.mem.Allocator, reader: anytype, max_size: u64) ReadError!IconDir {
14 // Some Reader implementations have an empty ReadError error set which would
15 // cause 'unreachable else' if we tried to use an else in the switch, so we
16 // need to detect this case and not try to translate to ReadError
17 const empty_reader_errorset = @typeInfo(@TypeOf(reader).Error).ErrorSet == null or @typeInfo(@TypeOf(reader).Error).ErrorSet.?.len == 0;
18 if (empty_reader_errorset) {
19 return readAnyError(allocator, reader, max_size) catch |err| switch (err) {
20 error.EndOfStream => error.UnexpectedEOF,
21 else => |e| return e,
22 };
23 } else {
24 return readAnyError(allocator, reader, max_size) catch |err| switch (err) {
25 error.OutOfMemory,
26 error.InvalidHeader,
27 error.InvalidImageType,
28 error.ImpossibleDataSize,
29 => |e| return e,
30 error.EndOfStream => error.UnexpectedEOF,
31 // The remaining errors are dependent on the `reader`, so
32 // we just translate them all to generic ReadError
33 else => error.ReadError,
34 };
35 }
36}
37
38// TODO: This seems like a somewhat strange pattern, could be a better way
39// to do this. Maybe it makes more sense to handle the translation
40// at the call site instead of having a helper function here.
41pub fn readAnyError(allocator: std.mem.Allocator, reader: anytype, max_size: u64) !IconDir {
42 const reserved = try reader.readInt(u16, .little);
43 if (reserved != 0) {
44 return error.InvalidHeader;
45 }
46
47 const image_type = reader.readEnum(ImageType, .little) catch |err| switch (err) {
48 error.InvalidValue => return error.InvalidImageType,
49 else => |e| return e,
50 };
51
52 const num_images = try reader.readInt(u16, .little);
53
54 // To avoid over-allocation in the case of a file that says it has way more
55 // entries than it actually does, we use an ArrayList with a conservatively
56 // limited initial capacity instead of allocating the entire slice at once.
57 const initial_capacity = @min(num_images, 8);
58 var entries = try std.ArrayList(Entry).initCapacity(allocator, initial_capacity);
59 errdefer entries.deinit();
60
61 var i: usize = 0;
62 while (i < num_images) : (i += 1) {
63 var entry: Entry = undefined;
64 entry.width = try reader.readByte();
65 entry.height = try reader.readByte();
66 entry.num_colors = try reader.readByte();
67 entry.reserved = try reader.readByte();
68 switch (image_type) {
69 .icon => {
70 entry.type_specific_data = .{ .icon = .{
71 .color_planes = try reader.readInt(u16, .little),
72 .bits_per_pixel = try reader.readInt(u16, .little),
73 } };
74 },
75 .cursor => {
76 entry.type_specific_data = .{ .cursor = .{
77 .hotspot_x = try reader.readInt(u16, .little),
78 .hotspot_y = try reader.readInt(u16, .little),
79 } };
80 },
81 }
82 entry.data_size_in_bytes = try reader.readInt(u32, .little);
83 entry.data_offset_from_start_of_file = try reader.readInt(u32, .little);
84 // Validate that the offset/data size is feasible
85 if (@as(u64, entry.data_offset_from_start_of_file) + entry.data_size_in_bytes > max_size) {
86 return error.ImpossibleDataSize;
87 }
88 // and that the data size is large enough for at least the header of an image
89 // Note: This avoids needing to deal with a miscompilation from the Win32 RC
90 // compiler when the data size of an image is specified as zero but there
91 // is data to-be-read at the offset. The Win32 RC compiler will output
92 // an ICON/CURSOR resource with a bogus size in its header but with no actual
93 // data bytes in it, leading to an invalid .res. Similarly, if, for example,
94 // there is valid PNG data at the image's offset, but the size is specified
95 // as fewer bytes than the PNG header, then the Win32 RC compiler will still
96 // treat it as a PNG (e.g. unconditionally set num_planes to 1) but the data
97 // of the resource will only be 1 byte so treating it as a PNG doesn't make
98 // sense (especially not when you have to read past the data size to determine
99 // that it's a PNG).
100 if (entry.data_size_in_bytes < 16) {
101 return error.ImpossibleDataSize;
102 }
103 try entries.append(entry);
104 }
105
106 return .{
107 .image_type = image_type,
108 .entries = try entries.toOwnedSlice(),
109 .allocator = allocator,
110 };
111}
112
113pub const ImageType = enum(u16) {
114 icon = 1,
115 cursor = 2,
116};
117
118pub const IconDir = struct {
119 image_type: ImageType,
120 /// Note: entries.len will always fit into a u16, since the field containing the
121 /// number of images in an ico file is a u16.
122 entries: []Entry,
123 allocator: std.mem.Allocator,
124
125 pub fn deinit(self: IconDir) void {
126 self.allocator.free(self.entries);
127 }
128
129 pub const res_header_byte_len = 6;
130
131 pub fn getResDataSize(self: IconDir) u32 {
132 // maxInt(u16) * Entry.res_byte_len = 917,490 which is well within the u32 range.
133 // Note: self.entries.len is limited to maxInt(u16)
134 return @intCast(IconDir.res_header_byte_len + self.entries.len * Entry.res_byte_len);
135 }
136
137 pub fn writeResData(self: IconDir, writer: anytype, first_image_id: u16) !void {
138 try writer.writeInt(u16, 0, .little);
139 try writer.writeInt(u16, @intFromEnum(self.image_type), .little);
140 // We know that entries.len must fit into a u16
141 try writer.writeInt(u16, @as(u16, @intCast(self.entries.len)), .little);
142
143 var image_id = first_image_id;
144 for (self.entries) |entry| {
145 try entry.writeResData(writer, image_id);
146 image_id += 1;
147 }
148 }
149};
150
151pub const Entry = struct {
152 // Icons are limited to u8 sizes, cursors can have u16,
153 // so we store as u16 and truncate when needed.
154 width: u16,
155 height: u16,
156 num_colors: u8,
157 /// This should always be zero, but whatever value it is gets
158 /// carried over so we need to store it
159 reserved: u8,
160 type_specific_data: union(ImageType) {
161 icon: struct {
162 color_planes: u16,
163 bits_per_pixel: u16,
164 },
165 cursor: struct {
166 hotspot_x: u16,
167 hotspot_y: u16,
168 },
169 },
170 data_size_in_bytes: u32,
171 data_offset_from_start_of_file: u32,
172
173 pub const res_byte_len = 14;
174
175 pub fn writeResData(self: Entry, writer: anytype, id: u16) !void {
176 switch (self.type_specific_data) {
177 .icon => |icon_data| {
178 try writer.writeInt(u8, @as(u8, @truncate(self.width)), .little);
179 try writer.writeInt(u8, @as(u8, @truncate(self.height)), .little);
180 try writer.writeInt(u8, self.num_colors, .little);
181 try writer.writeInt(u8, self.reserved, .little);
182 try writer.writeInt(u16, icon_data.color_planes, .little);
183 try writer.writeInt(u16, icon_data.bits_per_pixel, .little);
184 try writer.writeInt(u32, self.data_size_in_bytes, .little);
185 },
186 .cursor => |cursor_data| {
187 try writer.writeInt(u16, self.width, .little);
188 try writer.writeInt(u16, self.height, .little);
189 try writer.writeInt(u16, cursor_data.hotspot_x, .little);
190 try writer.writeInt(u16, cursor_data.hotspot_y, .little);
191 try writer.writeInt(u32, self.data_size_in_bytes + 4, .little);
192 },
193 }
194 try writer.writeInt(u16, id, .little);
195 }
196};
197
198test "icon" {
199 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;
200 var fbs = std.io.fixedBufferStream(data);
201 const icon = try read(std.testing.allocator, fbs.reader(), data.len);
202 defer icon.deinit();
203
204 try std.testing.expectEqual(ImageType.icon, icon.image_type);
205 try std.testing.expectEqual(@as(usize, 1), icon.entries.len);
206}
207
208test "icon too many images" {
209 // Note that with verifying that all data sizes are within the file bounds and >= 16,
210 // it's not possible to hit EOF when looking for more RESDIR structures, since they are
211 // themselves 16 bytes long, so we'll always hit ImpossibleDataSize instead.
212 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;
213 var fbs = std.io.fixedBufferStream(data);
214 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
215}
216
217test "icon data size past EOF" {
218 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;
219 var fbs = std.io.fixedBufferStream(data);
220 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
221}
222
223test "icon data offset past EOF" {
224 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;
225 var fbs = std.io.fixedBufferStream(data);
226 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
227}
228
229test "icon data size too small" {
230 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x0F\x00\x00\x00\x16\x00\x00\x00";
231 var fbs = std.io.fixedBufferStream(data);
232 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
233}
234
235pub const ImageFormat = enum {
236 dib,
237 png,
238 riff,
239
240 const riff_header = std.mem.readInt(u32, "RIFF", native_endian);
241 const png_signature = std.mem.readInt(u64, "\x89PNG\r\n\x1a\n", native_endian);
242 const ihdr_code = std.mem.readInt(u32, "IHDR", native_endian);
243 const acon_form_type = std.mem.readInt(u32, "ACON", native_endian);
244
245 pub fn detect(header_bytes: *const [16]u8) ImageFormat {
246 if (std.mem.readInt(u32, header_bytes[0..4], native_endian) == riff_header) return .riff;
247 if (std.mem.readInt(u64, header_bytes[0..8], native_endian) == png_signature) return .png;
248 return .dib;
249 }
250
251 pub fn validate(format: ImageFormat, header_bytes: *const [16]u8) bool {
252 return switch (format) {
253 .png => std.mem.readInt(u32, header_bytes[12..16], native_endian) == ihdr_code,
254 .riff => std.mem.readInt(u32, header_bytes[8..12], native_endian) == acon_form_type,
255 .dib => true,
256 };
257 }
258};
259
260/// Contains only the fields of BITMAPINFOHEADER (WinGDI.h) that are both:
261/// - relevant to what we need, and
262/// - are shared between all versions of BITMAPINFOHEADER (V4, V5).
263pub const BitmapHeader = extern struct {
264 bcSize: u32,
265 bcWidth: i32,
266 bcHeight: i32,
267 bcPlanes: u16,
268 bcBitCount: u16,
269
270 pub fn version(self: *const BitmapHeader) Version {
271 return Version.get(self.bcSize);
272 }
273
274 /// https://en.wikipedia.org/wiki/BMP_file_format#DIB_header_(bitmap_information_header)
275 pub const Version = enum {
276 unknown,
277 @"win2.0", // Windows 2.0 or later
278 @"nt3.1", // Windows NT, 3.1x or later
279 @"nt4.0", // Windows NT 4.0, 95 or later
280 @"nt5.0", // Windows NT 5.0, 98 or later
281
282 pub fn get(header_size: u32) Version {
283 return switch (header_size) {
284 len(.@"win2.0") => .@"win2.0",
285 len(.@"nt3.1") => .@"nt3.1",
286 len(.@"nt4.0") => .@"nt4.0",
287 len(.@"nt5.0") => .@"nt5.0",
288 else => .unknown,
289 };
290 }
291
292 pub fn len(comptime v: Version) comptime_int {
293 return switch (v) {
294 .@"win2.0" => 12,
295 .@"nt3.1" => 40,
296 .@"nt4.0" => 108,
297 .@"nt5.0" => 124,
298 .unknown => unreachable,
299 };
300 }
301
302 pub fn nameForErrorDisplay(v: Version) []const u8 {
303 return switch (v) {
304 .unknown => "unknown",
305 .@"win2.0" => "Windows 2.0 (BITMAPCOREHEADER)",
306 .@"nt3.1" => "Windows NT, 3.1x (BITMAPINFOHEADER)",
307 .@"nt4.0" => "Windows NT 4.0, 95 (BITMAPV4HEADER)",
308 .@"nt5.0" => "Windows NT 5.0, 98 (BITMAPV5HEADER)",
309 };
310 }
311 };
312};
lib/compiler/resinator/lang.zig created+877
...@@ -0,0 +1,877 @@
1const std = @import("std");
2
3/// This function is specific to how the Win32 RC command line interprets
4/// language IDs specified as integers.
5/// - Always interpreted as hexadecimal, but explicit 0x prefix is also allowed
6/// - Wraps on overflow of u16
7/// - Stops parsing on any invalid hexadecimal digits
8/// - Errors if a digit is not the first char
9/// - `-` (negative) prefix is allowed
10pub fn parseInt(str: []const u8) error{InvalidLanguageId}!u16 {
11 var result: u16 = 0;
12 const radix: u8 = 16;
13 var buf = str;
14
15 const Prefix = enum { none, minus };
16 var prefix: Prefix = .none;
17 switch (buf[0]) {
18 '-' => {
19 prefix = .minus;
20 buf = buf[1..];
21 },
22 else => {},
23 }
24
25 if (buf.len > 2 and buf[0] == '0' and buf[1] == 'x') {
26 buf = buf[2..];
27 }
28
29 for (buf, 0..) |c, i| {
30 const digit = switch (c) {
31 // On invalid digit for the radix, just stop parsing but don't fail
32 'a'...'f', 'A'...'F', '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
33 else => {
34 // First digit must be valid
35 if (i == 0) {
36 return error.InvalidLanguageId;
37 }
38 break;
39 },
40 };
41
42 if (result != 0) {
43 result *%= radix;
44 }
45 result +%= digit;
46 }
47
48 switch (prefix) {
49 .none => {},
50 .minus => result = 0 -% result,
51 }
52
53 return result;
54}
55
56test parseInt {
57 try std.testing.expectEqual(@as(u16, 0x16), try parseInt("16"));
58 try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1A"));
59 try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1Azzzz"));
60 try std.testing.expectEqual(@as(u16, 0xffff), try parseInt("-1"));
61 try std.testing.expectEqual(@as(u16, 0xffea), try parseInt("-0x16"));
62 try std.testing.expectEqual(@as(u16, 0x0), try parseInt("0o100"));
63 try std.testing.expectEqual(@as(u16, 0x1), try parseInt("10001"));
64 try std.testing.expectError(error.InvalidLanguageId, parseInt("--1"));
65 try std.testing.expectError(error.InvalidLanguageId, parseInt("0xha"));
66 try std.testing.expectError(error.InvalidLanguageId, parseInt("¹"));
67 try std.testing.expectError(error.InvalidLanguageId, parseInt("~1"));
68}
69
70/// This function is specific to how the Win32 RC command line interprets
71/// language tags: invalid tags are rejected, but tags that don't have
72/// a specific assigned ID but are otherwise valid enough will get
73/// converted to an ID of LOCALE_CUSTOM_UNSPECIFIED.
74pub fn tagToInt(tag: []const u8) error{InvalidLanguageTag}!u16 {
75 const maybe_id = try tagToId(tag);
76 if (maybe_id) |id| {
77 return @intFromEnum(id);
78 } else {
79 return LOCALE_CUSTOM_UNSPECIFIED;
80 }
81}
82
83pub fn tagToId(tag: []const u8) error{InvalidLanguageTag}!?LanguageId {
84 const parsed = try parse(tag);
85 // There are currently no language tags with assigned IDs that have
86 // multiple suffixes, so we can skip the lookup.
87 if (parsed.multiple_suffixes) return null;
88 const longest_known_tag = comptime blk: {
89 var len = 0;
90 for (@typeInfo(LanguageId).Enum.fields) |field| {
91 if (field.name.len > len) len = field.name.len;
92 }
93 break :blk len;
94 };
95 // If the tag is longer than the longest tag that has an assigned ID,
96 // then we can skip the lookup.
97 if (tag.len > longest_known_tag) return null;
98 var normalized_buf: [longest_known_tag]u8 = undefined;
99 // To allow e.g. `de-de_phoneb` to get looked up as `de-de`, we need to
100 // omit the suffix, but only if the tag contains a valid alternate sort order.
101 const tag_to_normalize = if (parsed.isSuffixValidSortOrder()) tag[0 .. tag.len - (parsed.suffix.?.len + 1)] else tag;
102 const normalized_tag = normalizeTag(tag_to_normalize, &normalized_buf);
103 return std.meta.stringToEnum(LanguageId, normalized_tag) orelse {
104 // special case for a tag that has been mapped to the same ID
105 // twice.
106 if (std.mem.eql(u8, "ff_latn_ng", normalized_tag)) {
107 return LanguageId.ff_ng;
108 }
109 return null;
110 };
111}
112
113test tagToId {
114 try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("ar-ae")).?);
115 try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("AR_AE")).?);
116 try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-ng")).?);
117 // Special case
118 try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-Latn-NG")).?);
119}
120
121test "exhaustive tagToId" {
122 inline for (@typeInfo(LanguageId).Enum.fields) |field| {
123 const id = tagToId(field.name) catch |err| {
124 std.debug.print("tag: {s}\n", .{field.name});
125 return err;
126 };
127 try std.testing.expectEqual(@field(LanguageId, field.name), id orelse {
128 std.debug.print("tag: {s}, got null\n", .{field.name});
129 return error.TestExpectedEqual;
130 });
131 }
132 var buf: [32]u8 = undefined;
133 inline for (valid_alternate_sorts) |parsed_sort| {
134 var fbs = std.io.fixedBufferStream(&buf);
135 const writer = fbs.writer();
136 writer.writeAll(parsed_sort.language_code) catch unreachable;
137 writer.writeAll("-") catch unreachable;
138 writer.writeAll(parsed_sort.country_code.?) catch unreachable;
139 writer.writeAll("-") catch unreachable;
140 writer.writeAll(parsed_sort.suffix.?) catch unreachable;
141 const expected_field_name = comptime field: {
142 var name_buf: [5]u8 = undefined;
143 @memcpy(name_buf[0..parsed_sort.language_code.len], parsed_sort.language_code);
144 name_buf[2] = '_';
145 @memcpy(name_buf[3..], parsed_sort.country_code.?);
146 break :field name_buf;
147 };
148 const expected = @field(LanguageId, &expected_field_name);
149 const id = tagToId(fbs.getWritten()) catch |err| {
150 std.debug.print("tag: {s}\n", .{fbs.getWritten()});
151 return err;
152 };
153 try std.testing.expectEqual(expected, id orelse {
154 std.debug.print("tag: {s}, expected: {}, got null\n", .{ fbs.getWritten(), expected });
155 return error.TestExpectedEqual;
156 });
157 }
158}
159
160fn normalizeTag(tag: []const u8, buf: []u8) []u8 {
161 std.debug.assert(buf.len >= tag.len);
162 for (tag, 0..) |c, i| {
163 if (c == '-')
164 buf[i] = '_'
165 else
166 buf[i] = std.ascii.toLower(c);
167 }
168 return buf[0..tag.len];
169}
170
171/// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-LCID/%5bMS-LCID%5d.pdf#%5B%7B%22num%22%3A72%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C69%2C574%2C0%5D
172/// "When an LCID is requested for a locale without a
173/// permanent LCID assignment, nor a temporary
174/// assignment as above, the protocol will respond
175/// with LOCALE_CUSTOM_UNSPECIFIED for all such
176/// locales. Because this single value is used for
177/// numerous possible locale names, it is impossible to
178/// round trip this locale, even temporarily.
179/// Applications should discard this value as soon as
180/// possible and never persist it. If the system is
181/// forced to respond to a request for
182/// LCID_CUSTOM_UNSPECIFIED, it will fall back to
183/// the current user locale. This is often incorrect but
184/// may prevent an application or component from
185/// failing. As the meaning of this temporary LCID is
186/// unstable, it should never be used for interchange
187/// or persisted data. This is a 1-to-many relationship
188/// that is very unstable."
189pub const LOCALE_CUSTOM_UNSPECIFIED = 0x1000;
190
191pub const LANG_ENGLISH = 0x09;
192pub const SUBLANG_ENGLISH_US = 0x01;
193
194/// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers
195pub fn MAKELANGID(primary: u10, sublang: u6) u16 {
196 return (@as(u16, primary) << 10) | sublang;
197}
198
199/// Language tag format expressed as a regular expression (rough approximation):
200///
201/// [a-zA-Z]{1,3}([-_][a-zA-Z]{4})?([-_][a-zA-Z]{2})?([-_][a-zA-Z0-9]{1,8})?
202/// lang | script | country | suffix
203///
204/// Notes:
205/// - If lang code is 1 char, it seems to mean that everything afterwards uses suffix
206/// parsing rules (e.g. `a-0` and `a-00000000` are allowed).
207/// - There can also be any number of trailing suffix parts as long as they each
208/// would be a valid suffix part, e.g. `en-us-blah-blah1-blah2-blah3` is allowed.
209/// - When doing lookups, trailing suffix parts are taken into account, e.g.
210/// `ca-es-valencia` is not considered equivalent to `ca-es-valencia-blah`.
211/// - A suffix is only allowed if:
212/// + Lang code is 1 char long, or
213/// + A country code is present, or
214/// + A script tag is not present and:
215/// - the suffix is numeric-only and has a length of 3, or
216/// - the lang is `qps` and the suffix is `ploca` or `plocm`
217pub fn parse(lang_tag: []const u8) error{InvalidLanguageTag}!Parsed {
218 var it = std.mem.splitAny(u8, lang_tag, "-_");
219 const lang_code = it.first();
220 const is_valid_lang_code = lang_code.len >= 1 and lang_code.len <= 3 and isAllAlphabetic(lang_code);
221 if (!is_valid_lang_code) return error.InvalidLanguageTag;
222 var parsed = Parsed{
223 .language_code = lang_code,
224 };
225 // The second part could be a script tag, a country code, or a suffix
226 if (it.next()) |part_str| {
227 // The lang code being length 1 behaves strangely, so fully special case it.
228 if (lang_code.len == 1) {
229 // This is almost certainly not the 'right' way to do this, but I don't have a method
230 // to determine how exactly these language tags are parsed, and it seems like
231 // suffix parsing rules apply generally (digits allowed, length of 1 to 8).
232 //
233 // However, because we want to be able to lookup `x-iv-mathan` normally without
234 // `multiple_suffixes` being set to true, we need to make sure to treat two-length
235 // alphabetic parts as a country code.
236 if (part_str.len == 2 and isAllAlphabetic(part_str)) {
237 parsed.country_code = part_str;
238 }
239 // Everything else, though, we can just throw into the suffix as long as the normal
240 // rules apply.
241 else if (part_str.len > 0 and part_str.len <= 8 and isAllAlphanumeric(part_str)) {
242 parsed.suffix = part_str;
243 } else {
244 return error.InvalidLanguageTag;
245 }
246 } else if (part_str.len == 4 and isAllAlphabetic(part_str)) {
247 parsed.script_tag = part_str;
248 } else if (part_str.len == 2 and isAllAlphabetic(part_str)) {
249 parsed.country_code = part_str;
250 }
251 // Only a 3-len numeric suffix is allowed as the second part of a tag
252 else if (part_str.len == 3 and isAllNumeric(part_str)) {
253 parsed.suffix = part_str;
254 }
255 // Special case for qps-ploca and qps-plocm
256 else if (std.ascii.eqlIgnoreCase(lang_code, "qps") and
257 (std.ascii.eqlIgnoreCase(part_str, "ploca") or
258 std.ascii.eqlIgnoreCase(part_str, "plocm")))
259 {
260 parsed.suffix = part_str;
261 } else {
262 return error.InvalidLanguageTag;
263 }
264 } else {
265 // If there's no part besides a 1-len lang code, then it is malformed
266 if (lang_code.len == 1) return error.InvalidLanguageTag;
267 return parsed;
268 }
269 if (parsed.script_tag != null) {
270 if (it.next()) |part_str| {
271 if (part_str.len == 2 and isAllAlphabetic(part_str)) {
272 parsed.country_code = part_str;
273 } else {
274 // Suffix is not allowed when a country code is not present.
275 return error.InvalidLanguageTag;
276 }
277 } else {
278 return parsed;
279 }
280 }
281 // We've now parsed any potential script tag/country codes, so anything remaining
282 // is a suffix
283 while (it.next()) |part_str| {
284 if (part_str.len == 0 or part_str.len > 8 or !isAllAlphanumeric(part_str)) {
285 return error.InvalidLanguageTag;
286 }
287 if (parsed.suffix == null) {
288 parsed.suffix = part_str;
289 } else {
290 // In theory we could return early here but we still want to validate
291 // that each part is a valid suffix all the way to the end, e.g.
292 // we should reject `en-us-suffix-a-b-c-!!!` because of the invalid `!!!`
293 // suffix part.
294 parsed.multiple_suffixes = true;
295 }
296 }
297 return parsed;
298}
299
300pub const Parsed = struct {
301 language_code: []const u8,
302 script_tag: ?[]const u8 = null,
303 country_code: ?[]const u8 = null,
304 /// Can be a sort order (e.g. phoneb) or something like valencia, 001, etc
305 suffix: ?[]const u8 = null,
306 /// There can be any number of suffixes, but we don't need to care what their
307 /// values are, we just need to know if any exist so that e.g. `ca-es-valencia-blah`
308 /// can be seen as different from `ca-es-valencia`. Storing this as a bool
309 /// allows us to avoid needing either (a) dynamic allocation or (b) a limit to
310 /// the number of suffixes allowed when parsing.
311 multiple_suffixes: bool = false,
312
313 pub fn isSuffixValidSortOrder(self: Parsed) bool {
314 if (self.country_code == null) return false;
315 if (self.suffix == null) return false;
316 if (self.script_tag != null) return false;
317 if (self.multiple_suffixes) return false;
318 for (valid_alternate_sorts) |valid_sort| {
319 if (std.ascii.eqlIgnoreCase(valid_sort.language_code, self.language_code) and
320 std.ascii.eqlIgnoreCase(valid_sort.country_code.?, self.country_code.?) and
321 std.ascii.eqlIgnoreCase(valid_sort.suffix.?, self.suffix.?))
322 {
323 return true;
324 }
325 }
326 return false;
327 }
328};
329
330/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
331/// See the table following this text: "Alternate sorts can be selected by using one of the identifiers from the following table."
332const valid_alternate_sorts = [_]Parsed{
333 // Note: x-IV-mathan is omitted due to how lookups are implemented.
334 // This table is used to make e.g. `de-de_phoneb` get looked up
335 // as `de-de` (the suffix is omitted for the lookup), but x-iv-mathan
336 // instead needs to be looked up with the suffix included because
337 // `x-iv` is not a tag with an assigned ID.
338 .{ .language_code = "de", .country_code = "de", .suffix = "phoneb" },
339 .{ .language_code = "hu", .country_code = "hu", .suffix = "tchncl" },
340 .{ .language_code = "ka", .country_code = "ge", .suffix = "modern" },
341 .{ .language_code = "zh", .country_code = "cn", .suffix = "stroke" },
342 .{ .language_code = "zh", .country_code = "sg", .suffix = "stroke" },
343 .{ .language_code = "zh", .country_code = "mo", .suffix = "stroke" },
344 .{ .language_code = "zh", .country_code = "tw", .suffix = "pronun" },
345 .{ .language_code = "zh", .country_code = "tw", .suffix = "radstr" },
346 .{ .language_code = "ja", .country_code = "jp", .suffix = "radstr" },
347 .{ .language_code = "zh", .country_code = "hk", .suffix = "radstr" },
348 .{ .language_code = "zh", .country_code = "mo", .suffix = "radstr" },
349 .{ .language_code = "zh", .country_code = "cn", .suffix = "phoneb" },
350 .{ .language_code = "zh", .country_code = "sg", .suffix = "phoneb" },
351};
352
353test "parse" {
354 try std.testing.expectEqualDeep(Parsed{
355 .language_code = "en",
356 }, try parse("en"));
357 try std.testing.expectEqualDeep(Parsed{
358 .language_code = "en",
359 .country_code = "us",
360 }, try parse("en-us"));
361 try std.testing.expectEqualDeep(Parsed{
362 .language_code = "en",
363 .suffix = "123",
364 }, try parse("en-123"));
365 try std.testing.expectEqualDeep(Parsed{
366 .language_code = "en",
367 .suffix = "123",
368 .multiple_suffixes = true,
369 }, try parse("en-123-blah"));
370 try std.testing.expectEqualDeep(Parsed{
371 .language_code = "en",
372 .country_code = "us",
373 .suffix = "123",
374 .multiple_suffixes = true,
375 }, try parse("en-us_123-blah"));
376 try std.testing.expectEqualDeep(Parsed{
377 .language_code = "eng",
378 .script_tag = "Latn",
379 }, try parse("eng-Latn"));
380 try std.testing.expectEqualDeep(Parsed{
381 .language_code = "eng",
382 .script_tag = "Latn",
383 }, try parse("eng-Latn"));
384 try std.testing.expectEqualDeep(Parsed{
385 .language_code = "ff",
386 .script_tag = "Latn",
387 .country_code = "NG",
388 }, try parse("ff-Latn-NG"));
389 try std.testing.expectEqualDeep(Parsed{
390 .language_code = "qps",
391 .suffix = "Plocm",
392 }, try parse("qps-Plocm"));
393 try std.testing.expectEqualDeep(Parsed{
394 .language_code = "qps",
395 .suffix = "ploca",
396 }, try parse("qps-ploca"));
397 try std.testing.expectEqualDeep(Parsed{
398 .language_code = "x",
399 .country_code = "IV",
400 .suffix = "mathan",
401 }, try parse("x-IV-mathan"));
402 try std.testing.expectEqualDeep(Parsed{
403 .language_code = "a",
404 .suffix = "a",
405 }, try parse("a-a"));
406 try std.testing.expectEqualDeep(Parsed{
407 .language_code = "a",
408 .suffix = "000",
409 }, try parse("a-000"));
410 try std.testing.expectEqualDeep(Parsed{
411 .language_code = "a",
412 .suffix = "00000000",
413 }, try parse("a-00000000"));
414 // suffix not allowed if script tag is present without country code
415 try std.testing.expectError(error.InvalidLanguageTag, parse("eng-Latn-suffix"));
416 // suffix must be 3 numeric digits if neither script tag nor country code is present
417 try std.testing.expectError(error.InvalidLanguageTag, parse("eng-suffix"));
418 try std.testing.expectError(error.InvalidLanguageTag, parse("en-plocm"));
419 // 1-len lang code is not allowed if it's the only part
420 try std.testing.expectError(error.InvalidLanguageTag, parse("e"));
421}
422
423fn isAllAlphabetic(str: []const u8) bool {
424 for (str) |c| {
425 if (!std.ascii.isAlphabetic(c)) return false;
426 }
427 return true;
428}
429
430fn isAllAlphanumeric(str: []const u8) bool {
431 for (str) |c| {
432 if (!std.ascii.isAlphanumeric(c)) return false;
433 }
434 return true;
435}
436
437fn isAllNumeric(str: []const u8) bool {
438 for (str) |c| {
439 if (!std.ascii.isDigit(c)) return false;
440 }
441 return true;
442}
443
444/// Derived from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
445/// - Protocol Revision: 15.0
446/// - Language / Language ID / Language Tag table in Appendix A
447/// - Removed all rows that have Language ID 0x1000 (LOCALE_CUSTOM_UNSPECIFIED)
448/// - Normalized each language tag (lowercased, replaced all `-` with `_`)
449/// - There is one special case where two tags are mapped to the same ID, the following
450/// has been omitted and must be special cased during lookup to map to the ID ff_ng / 0x0467.
451/// ff_latn_ng = 0x0467, // Fulah (Latin), Nigeria
452/// - x_iv_mathan has been added which is not in the table but does appear in the Alternate sorts
453/// table as 0x007F (LANG_INVARIANT).
454pub const LanguageId = enum(u16) {
455 // Language tag = Language ID, // Language, Location (or type)
456 af = 0x0036, // Afrikaans
457 af_za = 0x0436, // Afrikaans, South Africa
458 sq = 0x001C, // Albanian
459 sq_al = 0x041C, // Albanian, Albania
460 gsw = 0x0084, // Alsatian
461 gsw_fr = 0x0484, // Alsatian, France
462 am = 0x005E, // Amharic
463 am_et = 0x045E, // Amharic, Ethiopia
464 ar = 0x0001, // Arabic
465 ar_dz = 0x1401, // Arabic, Algeria
466 ar_bh = 0x3C01, // Arabic, Bahrain
467 ar_eg = 0x0c01, // Arabic, Egypt
468 ar_iq = 0x0801, // Arabic, Iraq
469 ar_jo = 0x2C01, // Arabic, Jordan
470 ar_kw = 0x3401, // Arabic, Kuwait
471 ar_lb = 0x3001, // Arabic, Lebanon
472 ar_ly = 0x1001, // Arabic, Libya
473 ar_ma = 0x1801, // Arabic, Morocco
474 ar_om = 0x2001, // Arabic, Oman
475 ar_qa = 0x4001, // Arabic, Qatar
476 ar_sa = 0x0401, // Arabic, Saudi Arabia
477 ar_sy = 0x2801, // Arabic, Syria
478 ar_tn = 0x1C01, // Arabic, Tunisia
479 ar_ae = 0x3801, // Arabic, U.A.E.
480 ar_ye = 0x2401, // Arabic, Yemen
481 hy = 0x002B, // Armenian
482 hy_am = 0x042B, // Armenian, Armenia
483 as = 0x004D, // Assamese
484 as_in = 0x044D, // Assamese, India
485 az_cyrl = 0x742C, // Azerbaijani (Cyrillic)
486 az_cyrl_az = 0x082C, // Azerbaijani (Cyrillic), Azerbaijan
487 az = 0x002C, // Azerbaijani (Latin)
488 az_latn = 0x782C, // Azerbaijani (Latin)
489 az_latn_az = 0x042C, // Azerbaijani (Latin), Azerbaijan
490 bn = 0x0045, // Bangla
491 bn_bd = 0x0845, // Bangla, Bangladesh
492 bn_in = 0x0445, // Bangla, India
493 ba = 0x006D, // Bashkir
494 ba_ru = 0x046D, // Bashkir, Russia
495 eu = 0x002D, // Basque
496 eu_es = 0x042D, // Basque, Spain
497 be = 0x0023, // Belarusian
498 be_by = 0x0423, // Belarusian, Belarus
499 bs_cyrl = 0x641A, // Bosnian (Cyrillic)
500 bs_cyrl_ba = 0x201A, // Bosnian (Cyrillic), Bosnia and Herzegovina
501 bs_latn = 0x681A, // Bosnian (Latin)
502 bs = 0x781A, // Bosnian (Latin)
503 bs_latn_ba = 0x141A, // Bosnian (Latin), Bosnia and Herzegovina
504 br = 0x007E, // Breton
505 br_fr = 0x047E, // Breton, France
506 bg = 0x0002, // Bulgarian
507 bg_bg = 0x0402, // Bulgarian, Bulgaria
508 my = 0x0055, // Burmese
509 my_mm = 0x0455, // Burmese, Myanmar
510 ca = 0x0003, // Catalan
511 ca_es = 0x0403, // Catalan, Spain
512 tzm_arab_ma = 0x045F, // Central Atlas Tamazight (Arabic), Morocco
513 ku = 0x0092, // Central Kurdish
514 ku_arab = 0x7c92, // Central Kurdish
515 ku_arab_iq = 0x0492, // Central Kurdish, Iraq
516 chr = 0x005C, // Cherokee
517 chr_cher = 0x7c5C, // Cherokee
518 chr_cher_us = 0x045C, // Cherokee, United States
519 zh_hans = 0x0004, // Chinese (Simplified)
520 zh = 0x7804, // Chinese (Simplified)
521 zh_cn = 0x0804, // Chinese (Simplified), People's Republic of China
522 zh_sg = 0x1004, // Chinese (Simplified), Singapore
523 zh_hant = 0x7C04, // Chinese (Traditional)
524 zh_hk = 0x0C04, // Chinese (Traditional), Hong Kong S.A.R.
525 zh_mo = 0x1404, // Chinese (Traditional), Macao S.A.R.
526 zh_tw = 0x0404, // Chinese (Traditional), Taiwan
527 co = 0x0083, // Corsican
528 co_fr = 0x0483, // Corsican, France
529 hr = 0x001A, // Croatian
530 hr_hr = 0x041A, // Croatian, Croatia
531 hr_ba = 0x101A, // Croatian (Latin), Bosnia and Herzegovina
532 cs = 0x0005, // Czech
533 cs_cz = 0x0405, // Czech, Czech Republic
534 da = 0x0006, // Danish
535 da_dk = 0x0406, // Danish, Denmark
536 prs = 0x008C, // Dari
537 prs_af = 0x048C, // Dari, Afghanistan
538 dv = 0x0065, // Divehi
539 dv_mv = 0x0465, // Divehi, Maldives
540 nl = 0x0013, // Dutch
541 nl_be = 0x0813, // Dutch, Belgium
542 nl_nl = 0x0413, // Dutch, Netherlands
543 dz_bt = 0x0C51, // Dzongkha, Bhutan
544 en = 0x0009, // English
545 en_au = 0x0C09, // English, Australia
546 en_bz = 0x2809, // English, Belize
547 en_ca = 0x1009, // English, Canada
548 en_029 = 0x2409, // English, Caribbean
549 en_hk = 0x3C09, // English, Hong Kong
550 en_in = 0x4009, // English, India
551 en_ie = 0x1809, // English, Ireland
552 en_jm = 0x2009, // English, Jamaica
553 en_my = 0x4409, // English, Malaysia
554 en_nz = 0x1409, // English, New Zealand
555 en_ph = 0x3409, // English, Republic of the Philippines
556 en_sg = 0x4809, // English, Singapore
557 en_za = 0x1C09, // English, South Africa
558 en_tt = 0x2c09, // English, Trinidad and Tobago
559 en_ae = 0x4C09, // English, United Arab Emirates
560 en_gb = 0x0809, // English, United Kingdom
561 en_us = 0x0409, // English, United States
562 en_zw = 0x3009, // English, Zimbabwe
563 et = 0x0025, // Estonian
564 et_ee = 0x0425, // Estonian, Estonia
565 fo = 0x0038, // Faroese
566 fo_fo = 0x0438, // Faroese, Faroe Islands
567 fil = 0x0064, // Filipino
568 fil_ph = 0x0464, // Filipino, Philippines
569 fi = 0x000B, // Finnish
570 fi_fi = 0x040B, // Finnish, Finland
571 fr = 0x000C, // French
572 fr_be = 0x080C, // French, Belgium
573 fr_cm = 0x2c0C, // French, Cameroon
574 fr_ca = 0x0c0C, // French, Canada
575 fr_029 = 0x1C0C, // French, Caribbean
576 fr_cd = 0x240C, // French, Congo, DRC
577 fr_ci = 0x300C, // French, Côte d'Ivoire
578 fr_fr = 0x040C, // French, France
579 fr_ht = 0x3c0C, // French, Haiti
580 fr_lu = 0x140C, // French, Luxembourg
581 fr_ml = 0x340C, // French, Mali
582 fr_ma = 0x380C, // French, Morocco
583 fr_mc = 0x180C, // French, Principality of Monaco
584 fr_re = 0x200C, // French, Reunion
585 fr_sn = 0x280C, // French, Senegal
586 fr_ch = 0x100C, // French, Switzerland
587 fy = 0x0062, // Frisian
588 fy_nl = 0x0462, // Frisian, Netherlands
589 ff = 0x0067, // Fulah
590 ff_latn = 0x7C67, // Fulah (Latin)
591 ff_ng = 0x0467, // Fulah, Nigeria
592 ff_latn_sn = 0x0867, // Fulah, Senegal
593 gl = 0x0056, // Galician
594 gl_es = 0x0456, // Galician, Spain
595 ka = 0x0037, // Georgian
596 ka_ge = 0x0437, // Georgian, Georgia
597 de = 0x0007, // German
598 de_at = 0x0C07, // German, Austria
599 de_de = 0x0407, // German, Germany
600 de_li = 0x1407, // German, Liechtenstein
601 de_lu = 0x1007, // German, Luxembourg
602 de_ch = 0x0807, // German, Switzerland
603 el = 0x0008, // Greek
604 el_gr = 0x0408, // Greek, Greece
605 kl = 0x006F, // Greenlandic
606 kl_gl = 0x046F, // Greenlandic, Greenland
607 gn = 0x0074, // Guarani
608 gn_py = 0x0474, // Guarani, Paraguay
609 gu = 0x0047, // Gujarati
610 gu_in = 0x0447, // Gujarati, India
611 ha = 0x0068, // Hausa (Latin)
612 ha_latn = 0x7C68, // Hausa (Latin)
613 ha_latn_ng = 0x0468, // Hausa (Latin), Nigeria
614 haw = 0x0075, // Hawaiian
615 haw_us = 0x0475, // Hawaiian, United States
616 he = 0x000D, // Hebrew
617 he_il = 0x040D, // Hebrew, Israel
618 hi = 0x0039, // Hindi
619 hi_in = 0x0439, // Hindi, India
620 hu = 0x000E, // Hungarian
621 hu_hu = 0x040E, // Hungarian, Hungary
622 is = 0x000F, // Icelandic
623 is_is = 0x040F, // Icelandic, Iceland
624 ig = 0x0070, // Igbo
625 ig_ng = 0x0470, // Igbo, Nigeria
626 id = 0x0021, // Indonesian
627 id_id = 0x0421, // Indonesian, Indonesia
628 iu = 0x005D, // Inuktitut (Latin)
629 iu_latn = 0x7C5D, // Inuktitut (Latin)
630 iu_latn_ca = 0x085D, // Inuktitut (Latin), Canada
631 iu_cans = 0x785D, // Inuktitut (Syllabics)
632 iu_cans_ca = 0x045d, // Inuktitut (Syllabics), Canada
633 ga = 0x003C, // Irish
634 ga_ie = 0x083C, // Irish, Ireland
635 it = 0x0010, // Italian
636 it_it = 0x0410, // Italian, Italy
637 it_ch = 0x0810, // Italian, Switzerland
638 ja = 0x0011, // Japanese
639 ja_jp = 0x0411, // Japanese, Japan
640 kn = 0x004B, // Kannada
641 kn_in = 0x044B, // Kannada, India
642 kr_latn_ng = 0x0471, // Kanuri (Latin), Nigeria
643 ks = 0x0060, // Kashmiri
644 ks_arab = 0x0460, // Kashmiri, Perso-Arabic
645 ks_deva_in = 0x0860, // Kashmiri (Devanagari), India
646 kk = 0x003F, // Kazakh
647 kk_kz = 0x043F, // Kazakh, Kazakhstan
648 km = 0x0053, // Khmer
649 km_kh = 0x0453, // Khmer, Cambodia
650 quc = 0x0086, // K'iche
651 quc_latn_gt = 0x0486, // K'iche, Guatemala
652 rw = 0x0087, // Kinyarwanda
653 rw_rw = 0x0487, // Kinyarwanda, Rwanda
654 sw = 0x0041, // Kiswahili
655 sw_ke = 0x0441, // Kiswahili, Kenya
656 kok = 0x0057, // Konkani
657 kok_in = 0x0457, // Konkani, India
658 ko = 0x0012, // Korean
659 ko_kr = 0x0412, // Korean, Korea
660 ky = 0x0040, // Kyrgyz
661 ky_kg = 0x0440, // Kyrgyz, Kyrgyzstan
662 lo = 0x0054, // Lao
663 lo_la = 0x0454, // Lao, Lao P.D.R.
664 la_va = 0x0476, // Latin, Vatican City
665 lv = 0x0026, // Latvian
666 lv_lv = 0x0426, // Latvian, Latvia
667 lt = 0x0027, // Lithuanian
668 lt_lt = 0x0427, // Lithuanian, Lithuania
669 dsb = 0x7C2E, // Lower Sorbian
670 dsb_de = 0x082E, // Lower Sorbian, Germany
671 lb = 0x006E, // Luxembourgish
672 lb_lu = 0x046E, // Luxembourgish, Luxembourg
673 mk = 0x002F, // Macedonian
674 mk_mk = 0x042F, // Macedonian, North Macedonia
675 ms = 0x003E, // Malay
676 ms_bn = 0x083E, // Malay, Brunei Darussalam
677 ms_my = 0x043E, // Malay, Malaysia
678 ml = 0x004C, // Malayalam
679 ml_in = 0x044C, // Malayalam, India
680 mt = 0x003A, // Maltese
681 mt_mt = 0x043A, // Maltese, Malta
682 mi = 0x0081, // Maori
683 mi_nz = 0x0481, // Maori, New Zealand
684 arn = 0x007A, // Mapudungun
685 arn_cl = 0x047A, // Mapudungun, Chile
686 mr = 0x004E, // Marathi
687 mr_in = 0x044E, // Marathi, India
688 moh = 0x007C, // Mohawk
689 moh_ca = 0x047C, // Mohawk, Canada
690 mn = 0x0050, // Mongolian (Cyrillic)
691 mn_cyrl = 0x7850, // Mongolian (Cyrillic)
692 mn_mn = 0x0450, // Mongolian (Cyrillic), Mongolia
693 mn_mong = 0x7C50, // Mongolian (Traditional Mongolian)
694 mn_mong_cn = 0x0850, // Mongolian (Traditional Mongolian), People's Republic of China
695 mn_mong_mn = 0x0C50, // Mongolian (Traditional Mongolian), Mongolia
696 ne = 0x0061, // Nepali
697 ne_in = 0x0861, // Nepali, India
698 ne_np = 0x0461, // Nepali, Nepal
699 no = 0x0014, // Norwegian (Bokmal)
700 nb = 0x7C14, // Norwegian (Bokmal)
701 nb_no = 0x0414, // Norwegian (Bokmal), Norway
702 nn = 0x7814, // Norwegian (Nynorsk)
703 nn_no = 0x0814, // Norwegian (Nynorsk), Norway
704 oc = 0x0082, // Occitan
705 oc_fr = 0x0482, // Occitan, France
706 @"or" = 0x0048, // Odia
707 or_in = 0x0448, // Odia, India
708 om = 0x0072, // Oromo
709 om_et = 0x0472, // Oromo, Ethiopia
710 ps = 0x0063, // Pashto
711 ps_af = 0x0463, // Pashto, Afghanistan
712 fa = 0x0029, // Persian
713 fa_ir = 0x0429, // Persian, Iran
714 pl = 0x0015, // Polish
715 pl_pl = 0x0415, // Polish, Poland
716 pt = 0x0016, // Portuguese
717 pt_br = 0x0416, // Portuguese, Brazil
718 pt_pt = 0x0816, // Portuguese, Portugal
719 qps_ploca = 0x05FE, // Pseudo Language, Pseudo locale for east Asian/complex script localization testing
720 qps_ploc = 0x0501, // Pseudo Language, Pseudo locale used for localization testing
721 qps_plocm = 0x09FF, // Pseudo Language, Pseudo locale used for localization testing of mirrored locales
722 pa = 0x0046, // Punjabi
723 pa_arab = 0x7C46, // Punjabi
724 pa_in = 0x0446, // Punjabi, India
725 pa_arab_pk = 0x0846, // Punjabi, Islamic Republic of Pakistan
726 quz = 0x006B, // Quechua
727 quz_bo = 0x046B, // Quechua, Bolivia
728 quz_ec = 0x086B, // Quechua, Ecuador
729 quz_pe = 0x0C6B, // Quechua, Peru
730 ro = 0x0018, // Romanian
731 ro_md = 0x0818, // Romanian, Moldova
732 ro_ro = 0x0418, // Romanian, Romania
733 rm = 0x0017, // Romansh
734 rm_ch = 0x0417, // Romansh, Switzerland
735 ru = 0x0019, // Russian
736 ru_md = 0x0819, // Russian, Moldova
737 ru_ru = 0x0419, // Russian, Russia
738 sah = 0x0085, // Sakha
739 sah_ru = 0x0485, // Sakha, Russia
740 smn = 0x703B, // Sami (Inari)
741 smn_fi = 0x243B, // Sami (Inari), Finland
742 smj = 0x7C3B, // Sami (Lule)
743 smj_no = 0x103B, // Sami (Lule), Norway
744 smj_se = 0x143B, // Sami (Lule), Sweden
745 se = 0x003B, // Sami (Northern)
746 se_fi = 0x0C3B, // Sami (Northern), Finland
747 se_no = 0x043B, // Sami (Northern), Norway
748 se_se = 0x083B, // Sami (Northern), Sweden
749 sms = 0x743B, // Sami (Skolt)
750 sms_fi = 0x203B, // Sami (Skolt), Finland
751 sma = 0x783B, // Sami (Southern)
752 sma_no = 0x183B, // Sami (Southern), Norway
753 sma_se = 0x1C3B, // Sami (Southern), Sweden
754 sa = 0x004F, // Sanskrit
755 sa_in = 0x044F, // Sanskrit, India
756 gd = 0x0091, // Scottish Gaelic
757 gd_gb = 0x0491, // Scottish Gaelic, United Kingdom
758 sr_cyrl = 0x6C1A, // Serbian (Cyrillic)
759 sr_cyrl_ba = 0x1C1A, // Serbian (Cyrillic), Bosnia and Herzegovina
760 sr_cyrl_me = 0x301A, // Serbian (Cyrillic), Montenegro
761 sr_cyrl_rs = 0x281A, // Serbian (Cyrillic), Serbia
762 sr_cyrl_cs = 0x0C1A, // Serbian (Cyrillic), Serbia and Montenegro (Former)
763 sr_latn = 0x701A, // Serbian (Latin)
764 sr = 0x7C1A, // Serbian (Latin)
765 sr_latn_ba = 0x181A, // Serbian (Latin), Bosnia and Herzegovina
766 sr_latn_me = 0x2c1A, // Serbian (Latin), Montenegro
767 sr_latn_rs = 0x241A, // Serbian (Latin), Serbia
768 sr_latn_cs = 0x081A, // Serbian (Latin), Serbia and Montenegro (Former)
769 nso = 0x006C, // Sesotho sa Leboa
770 nso_za = 0x046C, // Sesotho sa Leboa, South Africa
771 tn = 0x0032, // Setswana
772 tn_bw = 0x0832, // Setswana, Botswana
773 tn_za = 0x0432, // Setswana, South Africa
774 sd = 0x0059, // Sindhi
775 sd_arab = 0x7C59, // Sindhi
776 sd_arab_pk = 0x0859, // Sindhi, Islamic Republic of Pakistan
777 si = 0x005B, // Sinhala
778 si_lk = 0x045B, // Sinhala, Sri Lanka
779 sk = 0x001B, // Slovak
780 sk_sk = 0x041B, // Slovak, Slovakia
781 sl = 0x0024, // Slovenian
782 sl_si = 0x0424, // Slovenian, Slovenia
783 so = 0x0077, // Somali
784 so_so = 0x0477, // Somali, Somalia
785 st = 0x0030, // Sotho
786 st_za = 0x0430, // Sotho, South Africa
787 es = 0x000A, // Spanish
788 es_ar = 0x2C0A, // Spanish, Argentina
789 es_ve = 0x200A, // Spanish, Bolivarian Republic of Venezuela
790 es_bo = 0x400A, // Spanish, Bolivia
791 es_cl = 0x340A, // Spanish, Chile
792 es_co = 0x240A, // Spanish, Colombia
793 es_cr = 0x140A, // Spanish, Costa Rica
794 es_cu = 0x5c0A, // Spanish, Cuba
795 es_do = 0x1c0A, // Spanish, Dominican Republic
796 es_ec = 0x300A, // Spanish, Ecuador
797 es_sv = 0x440A, // Spanish, El Salvador
798 es_gt = 0x100A, // Spanish, Guatemala
799 es_hn = 0x480A, // Spanish, Honduras
800 es_419 = 0x580A, // Spanish, Latin America
801 es_mx = 0x080A, // Spanish, Mexico
802 es_ni = 0x4C0A, // Spanish, Nicaragua
803 es_pa = 0x180A, // Spanish, Panama
804 es_py = 0x3C0A, // Spanish, Paraguay
805 es_pe = 0x280A, // Spanish, Peru
806 es_pr = 0x500A, // Spanish, Puerto Rico
807 es_es_tradnl = 0x040A, // Spanish, Spain
808 es_es = 0x0c0A, // Spanish, Spain
809 es_us = 0x540A, // Spanish, United States
810 es_uy = 0x380A, // Spanish, Uruguay
811 sv = 0x001D, // Swedish
812 sv_fi = 0x081D, // Swedish, Finland
813 sv_se = 0x041D, // Swedish, Sweden
814 syr = 0x005A, // Syriac
815 syr_sy = 0x045A, // Syriac, Syria
816 tg = 0x0028, // Tajik (Cyrillic)
817 tg_cyrl = 0x7C28, // Tajik (Cyrillic)
818 tg_cyrl_tj = 0x0428, // Tajik (Cyrillic), Tajikistan
819 tzm = 0x005F, // Tamazight (Latin)
820 tzm_latn = 0x7C5F, // Tamazight (Latin)
821 tzm_latn_dz = 0x085F, // Tamazight (Latin), Algeria
822 ta = 0x0049, // Tamil
823 ta_in = 0x0449, // Tamil, India
824 ta_lk = 0x0849, // Tamil, Sri Lanka
825 tt = 0x0044, // Tatar
826 tt_ru = 0x0444, // Tatar, Russia
827 te = 0x004A, // Telugu
828 te_in = 0x044A, // Telugu, India
829 th = 0x001E, // Thai
830 th_th = 0x041E, // Thai, Thailand
831 bo = 0x0051, // Tibetan
832 bo_cn = 0x0451, // Tibetan, People's Republic of China
833 ti = 0x0073, // Tigrinya
834 ti_er = 0x0873, // Tigrinya, Eritrea
835 ti_et = 0x0473, // Tigrinya, Ethiopia
836 ts = 0x0031, // Tsonga
837 ts_za = 0x0431, // Tsonga, South Africa
838 tr = 0x001F, // Turkish
839 tr_tr = 0x041F, // Turkish, Turkey
840 tk = 0x0042, // Turkmen
841 tk_tm = 0x0442, // Turkmen, Turkmenistan
842 uk = 0x0022, // Ukrainian
843 uk_ua = 0x0422, // Ukrainian, Ukraine
844 hsb = 0x002E, // Upper Sorbian
845 hsb_de = 0x042E, // Upper Sorbian, Germany
846 ur = 0x0020, // Urdu
847 ur_in = 0x0820, // Urdu, India
848 ur_pk = 0x0420, // Urdu, Islamic Republic of Pakistan
849 ug = 0x0080, // Uyghur
850 ug_cn = 0x0480, // Uyghur, People's Republic of China
851 uz_cyrl = 0x7843, // Uzbek (Cyrillic)
852 uz_cyrl_uz = 0x0843, // Uzbek (Cyrillic), Uzbekistan
853 uz = 0x0043, // Uzbek (Latin)
854 uz_latn = 0x7C43, // Uzbek (Latin)
855 uz_latn_uz = 0x0443, // Uzbek (Latin), Uzbekistan
856 ca_es_valencia = 0x0803, // Valencian, Spain
857 ve = 0x0033, // Venda
858 ve_za = 0x0433, // Venda, South Africa
859 vi = 0x002A, // Vietnamese
860 vi_vn = 0x042A, // Vietnamese, Vietnam
861 cy = 0x0052, // Welsh
862 cy_gb = 0x0452, // Welsh, United Kingdom
863 wo = 0x0088, // Wolof
864 wo_sn = 0x0488, // Wolof, Senegal
865 xh = 0x0034, // Xhosa
866 xh_za = 0x0434, // Xhosa, South Africa
867 ii = 0x0078, // Yi
868 ii_cn = 0x0478, // Yi, People's Republic of China
869 yi_001 = 0x043D, // Yiddish, World
870 yo = 0x006A, // Yoruba
871 yo_ng = 0x046A, // Yoruba, Nigeria
872 zu = 0x0035, // Zulu
873 zu_za = 0x0435, // Zulu, South Africa
874
875 /// Special case
876 x_iv_mathan = 0x007F, // LANG_INVARIANT, "math alphanumeric sorting"
877};
lib/compiler/resinator/lex.zig created+1106
...@@ -0,0 +1,1106 @@
1//! Expects to be run after the C preprocessor and after `removeComments`.
2//! This means that the lexer assumes that:
3//! - Splices ('\' at the end of a line) have been handled/collapsed.
4//! - Preprocessor directives and macros have been expanded (any remaining should be skipped with the exception of `#pragma code_page`).
5//! - All comments have been removed.
6
7const std = @import("std");
8const ErrorDetails = @import("errors.zig").ErrorDetails;
9const columnWidth = @import("literals.zig").columnWidth;
10const code_pages = @import("code_pages.zig");
11const CodePage = code_pages.CodePage;
12const SourceMappings = @import("source_mapping.zig").SourceMappings;
13const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
14
15const dumpTokensDuringTests = false;
16
17pub const default_max_string_literal_codepoints = 4097;
18
19pub const Token = struct {
20 id: Id,
21 start: usize,
22 end: usize,
23 line_number: usize,
24
25 pub const Id = enum {
26 literal,
27 number,
28 quoted_ascii_string,
29 quoted_wide_string,
30 operator,
31 begin,
32 end,
33 comma,
34 open_paren,
35 close_paren,
36 /// This Id is only used for errors, the Lexer will never return one
37 /// of these from a `next` call.
38 preprocessor_command,
39 invalid,
40 eof,
41
42 pub fn nameForErrorDisplay(self: Id) []const u8 {
43 return switch (self) {
44 .literal => "<literal>",
45 .number => "<number>",
46 .quoted_ascii_string => "<quoted ascii string>",
47 .quoted_wide_string => "<quoted wide string>",
48 .operator => "<operator>",
49 .begin => "<'{' or BEGIN>",
50 .end => "<'}' or END>",
51 .comma => ",",
52 .open_paren => "(",
53 .close_paren => ")",
54 .preprocessor_command => "<preprocessor command>",
55 .invalid => unreachable,
56 .eof => "<eof>",
57 };
58 }
59 };
60
61 pub fn slice(self: Token, buffer: []const u8) []const u8 {
62 return buffer[self.start..self.end];
63 }
64
65 pub fn nameForErrorDisplay(self: Token, buffer: []const u8) []const u8 {
66 return switch (self.id) {
67 .eof => self.id.nameForErrorDisplay(),
68 else => self.slice(buffer),
69 };
70 }
71
72 /// Returns 0-based column
73 pub fn calculateColumn(token: Token, source: []const u8, tab_columns: usize, maybe_line_start: ?usize) usize {
74 const line_start = maybe_line_start orelse token.getLineStartForColumnCalc(source);
75
76 var i: usize = line_start;
77 var column: usize = 0;
78 while (i < token.start) : (i += 1) {
79 column += columnWidth(column, source[i], tab_columns);
80 }
81 return column;
82 }
83
84 // TODO: More testing is needed to determine if this can be merged with getLineStartForErrorDisplay
85 // (the TODO in currentIndexFormsLineEndingPair should be taken into account as well)
86 pub fn getLineStartForColumnCalc(token: Token, source: []const u8) usize {
87 const line_start = line_start: {
88 if (token.start != 0) {
89 // start checking at the byte before the token
90 var index = token.start - 1;
91 while (true) {
92 if (source[index] == '\n') break :line_start @min(source.len - 1, index + 1);
93 if (index != 0) index -= 1 else break;
94 }
95 }
96 break :line_start 0;
97 };
98 return line_start;
99 }
100
101 pub fn getLineStartForErrorDisplay(token: Token, source: []const u8) usize {
102 const line_start = line_start: {
103 if (token.start != 0) {
104 // start checking at the byte before the token
105 var index = token.start - 1;
106 while (true) {
107 if (source[index] == '\r' or source[index] == '\n') break :line_start @min(source.len - 1, index + 1);
108 if (index != 0) index -= 1 else break;
109 }
110 }
111 break :line_start 0;
112 };
113 return line_start;
114 }
115
116 pub fn getLineForErrorDisplay(token: Token, source: []const u8, maybe_line_start: ?usize) []const u8 {
117 const line_start = maybe_line_start orelse token.getLineStartForErrorDisplay(source);
118
119 var line_end = line_start;
120 while (line_end < source.len and source[line_end] != '\r' and source[line_end] != '\n') : (line_end += 1) {}
121 return source[line_start..line_end];
122 }
123
124 pub fn isStringLiteral(token: Token) bool {
125 return token.id == .quoted_ascii_string or token.id == .quoted_wide_string;
126 }
127};
128
129pub const LineHandler = struct {
130 line_number: usize = 1,
131 buffer: []const u8,
132 last_line_ending_index: ?usize = null,
133
134 /// Like incrementLineNumber but checks that the current char is a line ending first.
135 /// Returns the new line number if it was incremented, null otherwise.
136 pub fn maybeIncrementLineNumber(self: *LineHandler, cur_index: usize) ?usize {
137 const c = self.buffer[cur_index];
138 if (c == '\r' or c == '\n') {
139 return self.incrementLineNumber(cur_index);
140 }
141 return null;
142 }
143
144 /// Increments line_number appropriately (handling line ending pairs)
145 /// and returns the new line number if it was incremented, or null otherwise.
146 pub fn incrementLineNumber(self: *LineHandler, cur_index: usize) ?usize {
147 if (self.currentIndexFormsLineEndingPair(cur_index)) {
148 self.last_line_ending_index = null;
149 return null;
150 } else {
151 self.line_number += 1;
152 self.last_line_ending_index = cur_index;
153 return self.line_number;
154 }
155 }
156
157 /// \r\n and \n\r pairs are treated as a single line ending (but not \r\r \n\n)
158 /// expects self.index and last_line_ending_index (if non-null) to contain line endings
159 ///
160 /// TODO: This is not really how the Win32 RC compiler handles line endings. Instead, it
161 /// seems to drop all carriage returns during preprocessing and then replace all
162 /// remaining line endings with well-formed CRLF pairs (e.g. `<CR>a<CR>b<LF>c` becomes `ab<CR><LF>c`).
163 /// Handling this the same as the Win32 RC compiler would need control over the preprocessor,
164 /// since Clang converts unpaired <CR> into unpaired <LF>.
165 pub fn currentIndexFormsLineEndingPair(self: *const LineHandler, cur_index: usize) bool {
166 if (self.last_line_ending_index == null) return false;
167
168 // must immediately precede the current index, we know cur_index must
169 // be >= 1 since last_line_ending_index is non-null (so if the subtraction
170 // overflows it is a bug at the callsite of this function).
171 if (self.last_line_ending_index.? != cur_index - 1) return false;
172
173 const cur_line_ending = self.buffer[cur_index];
174 const last_line_ending = self.buffer[self.last_line_ending_index.?];
175
176 // sanity check
177 std.debug.assert(cur_line_ending == '\r' or cur_line_ending == '\n');
178 std.debug.assert(last_line_ending == '\r' or last_line_ending == '\n');
179
180 // can't be \n\n or \r\r
181 if (last_line_ending == cur_line_ending) return false;
182
183 return true;
184 }
185};
186
187pub const LexError = error{
188 UnfinishedStringLiteral,
189 StringLiteralTooLong,
190 InvalidNumberWithExponent,
191 InvalidDigitCharacterInNumberLiteral,
192 IllegalByte,
193 IllegalByteOutsideStringLiterals,
194 IllegalCodepointOutsideStringLiterals,
195 IllegalByteOrderMark,
196 IllegalPrivateUseCharacter,
197 FoundCStyleEscapedQuote,
198 CodePagePragmaMissingLeftParen,
199 CodePagePragmaMissingRightParen,
200 /// Can be caught and ignored
201 CodePagePragmaInvalidCodePage,
202 CodePagePragmaNotInteger,
203 CodePagePragmaOverflow,
204 CodePagePragmaUnsupportedCodePage,
205 /// Can be caught and ignored
206 CodePagePragmaInIncludedFile,
207};
208
209pub const Lexer = struct {
210 const Self = @This();
211
212 buffer: []const u8,
213 index: usize,
214 line_handler: LineHandler,
215 at_start_of_line: bool = true,
216 error_context_token: ?Token = null,
217 current_code_page: CodePage,
218 default_code_page: CodePage,
219 source_mappings: ?*SourceMappings,
220 max_string_literal_codepoints: u15,
221 /// Needed to determine whether or not the output code page should
222 /// be set in the parser.
223 seen_pragma_code_pages: u2 = 0,
224
225 pub const Error = LexError;
226
227 pub const LexerOptions = struct {
228 default_code_page: CodePage = .windows1252,
229 source_mappings: ?*SourceMappings = null,
230 max_string_literal_codepoints: u15 = default_max_string_literal_codepoints,
231 };
232
233 pub fn init(buffer: []const u8, options: LexerOptions) Self {
234 return Self{
235 .buffer = buffer,
236 .index = 0,
237 .current_code_page = options.default_code_page,
238 .default_code_page = options.default_code_page,
239 .source_mappings = options.source_mappings,
240 .max_string_literal_codepoints = options.max_string_literal_codepoints,
241 .line_handler = .{ .buffer = buffer },
242 };
243 }
244
245 pub fn dump(self: *Self, token: *const Token) void {
246 std.debug.print("{s}:{d}: {s}\n", .{ @tagName(token.id), token.line_number, std.fmt.fmtSliceEscapeLower(token.slice(self.buffer)) });
247 }
248
249 pub const LexMethod = enum {
250 whitespace_delimiter_only,
251 normal,
252 normal_expect_operator,
253 };
254
255 pub fn next(self: *Self, comptime method: LexMethod) LexError!Token {
256 switch (method) {
257 .whitespace_delimiter_only => return self.nextWhitespaceDelimeterOnly(),
258 .normal => return self.nextNormal(),
259 .normal_expect_operator => return self.nextNormalWithContext(.expect_operator),
260 }
261 }
262
263 const StateWhitespaceDelimiterOnly = enum {
264 start,
265 literal,
266 preprocessor,
267 semicolon,
268 };
269
270 pub fn nextWhitespaceDelimeterOnly(self: *Self) LexError!Token {
271 const start_index = self.index;
272 var result = Token{
273 .id = .eof,
274 .start = start_index,
275 .end = undefined,
276 .line_number = self.line_handler.line_number,
277 };
278 var state = StateWhitespaceDelimiterOnly.start;
279
280 while (self.current_code_page.codepointAt(self.index, self.buffer)) |codepoint| : (self.index += codepoint.byte_len) {
281 const c = codepoint.value;
282 try self.checkForIllegalCodepoint(codepoint, false);
283 switch (state) {
284 .start => switch (c) {
285 '\r', '\n' => {
286 result.start = self.index + 1;
287 result.line_number = self.incrementLineNumber();
288 },
289 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
290 result.start = self.index + 1;
291 },
292 // NBSP only counts as whitespace at the start of a line (but
293 // can be intermixed with other whitespace). Who knows why.
294 '\xA0' => if (self.at_start_of_line) {
295 result.start = self.index + codepoint.byte_len;
296 } else {
297 state = .literal;
298 self.at_start_of_line = false;
299 },
300 '#' => {
301 if (self.at_start_of_line) {
302 state = .preprocessor;
303 } else {
304 state = .literal;
305 }
306 self.at_start_of_line = false;
307 },
308 // Semi-colon acts as a line-terminator, but in this lexing mode
309 // that's only true if it's at the start of a line.
310 ';' => {
311 if (self.at_start_of_line) {
312 state = .semicolon;
313 }
314 self.at_start_of_line = false;
315 },
316 else => {
317 state = .literal;
318 self.at_start_of_line = false;
319 },
320 },
321 .literal => switch (c) {
322 '\r', '\n', ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
323 result.id = .literal;
324 break;
325 },
326 else => {},
327 },
328 .preprocessor => switch (c) {
329 '\r', '\n' => {
330 try self.evaluatePreprocessorCommand(result.start, self.index);
331 result.start = self.index + 1;
332 state = .start;
333 result.line_number = self.incrementLineNumber();
334 },
335 else => {},
336 },
337 .semicolon => switch (c) {
338 '\r', '\n' => {
339 result.start = self.index + 1;
340 state = .start;
341 result.line_number = self.incrementLineNumber();
342 },
343 else => {},
344 },
345 }
346 } else { // got EOF
347 switch (state) {
348 .start, .semicolon => {},
349 .literal => {
350 result.id = .literal;
351 },
352 .preprocessor => {
353 try self.evaluatePreprocessorCommand(result.start, self.index);
354 result.start = self.index;
355 },
356 }
357 }
358
359 result.end = self.index;
360 return result;
361 }
362
363 const StateNormal = enum {
364 start,
365 literal_or_quoted_wide_string,
366 quoted_ascii_string,
367 quoted_wide_string,
368 quoted_ascii_string_escape,
369 quoted_wide_string_escape,
370 quoted_ascii_string_maybe_end,
371 quoted_wide_string_maybe_end,
372 literal,
373 number_literal,
374 preprocessor,
375 semicolon,
376 // end
377 e,
378 en,
379 // begin
380 b,
381 be,
382 beg,
383 begi,
384 };
385
386 /// TODO: A not-terrible name
387 pub fn nextNormal(self: *Self) LexError!Token {
388 return self.nextNormalWithContext(.any);
389 }
390
391 pub fn nextNormalWithContext(self: *Self, context: enum { expect_operator, any }) LexError!Token {
392 const start_index = self.index;
393 var result = Token{
394 .id = .eof,
395 .start = start_index,
396 .end = undefined,
397 .line_number = self.line_handler.line_number,
398 };
399 var state = StateNormal.start;
400
401 // Note: The Windows RC compiler uses a non-standard method of computing
402 // length for its 'string literal too long' errors; it isn't easily
403 // explained or intuitive (it's sort-of pre-parsed byte length but with
404 // a few of exceptions/edge cases).
405 //
406 // It also behaves strangely with non-ASCII codepoints, e.g. even though the default
407 // limit is 4097, you can only have 4094 € codepoints (1 UTF-16 code unit each),
408 // and 2048 𐐷 codepoints (2 UTF-16 code units each).
409 //
410 // TODO: Understand this more, bring it more in line with how the Win32 limits work.
411 // Alternatively, do something that makes more sense but may be more permissive.
412 var string_literal_length: usize = 0;
413 // Keeping track of the string literal column prevents pathological edge cases when
414 // there are tons of tab stop characters within a string literal.
415 var string_literal_column: usize = 0;
416 var string_literal_collapsing_whitespace: bool = false;
417 var still_could_have_exponent: bool = true;
418 var exponent_index: ?usize = null;
419 while (self.current_code_page.codepointAt(self.index, self.buffer)) |codepoint| : (self.index += codepoint.byte_len) {
420 const c = codepoint.value;
421 const in_string_literal = switch (state) {
422 .quoted_ascii_string,
423 .quoted_wide_string,
424 .quoted_ascii_string_escape,
425 .quoted_wide_string_escape,
426 .quoted_ascii_string_maybe_end,
427 .quoted_wide_string_maybe_end,
428 =>
429 // If the current line is not the same line as the start of the string literal,
430 // then we want to treat the current codepoint as 'not in a string literal'
431 // for the purposes of detecting illegal codepoints. This means that we will
432 // error on illegal-outside-string-literal characters that are outside string
433 // literals from the perspective of a C preprocessor, but that may be
434 // inside string literals from the perspective of the RC lexer. For example,
435 // "hello
436 // @"
437 // will be treated as a single string literal by the RC lexer but the Win32
438 // preprocessor will consider this an unclosed string literal followed by
439 // the character @ and ", and will therefore error since the Win32 RC preprocessor
440 // errors on the @ character outside string literals.
441 //
442 // By doing this here, we can effectively emulate the Win32 RC preprocessor behavior
443 // at lex-time, and avoid the need for a separate step that checks for this edge-case
444 // specifically.
445 result.line_number == self.line_handler.line_number,
446 else => false,
447 };
448 try self.checkForIllegalCodepoint(codepoint, in_string_literal);
449 switch (state) {
450 .start => switch (c) {
451 '\r', '\n' => {
452 result.start = self.index + 1;
453 result.line_number = self.incrementLineNumber();
454 },
455 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
456 result.start = self.index + 1;
457 },
458 // NBSP only counts as whitespace at the start of a line (but
459 // can be intermixed with other whitespace). Who knows why.
460 '\xA0' => if (self.at_start_of_line) {
461 result.start = self.index + codepoint.byte_len;
462 } else {
463 state = .literal;
464 self.at_start_of_line = false;
465 },
466 'L', 'l' => {
467 state = .literal_or_quoted_wide_string;
468 self.at_start_of_line = false;
469 },
470 'E', 'e' => {
471 state = .e;
472 self.at_start_of_line = false;
473 },
474 'B', 'b' => {
475 state = .b;
476 self.at_start_of_line = false;
477 },
478 '"' => {
479 state = .quoted_ascii_string;
480 self.at_start_of_line = false;
481 string_literal_collapsing_whitespace = false;
482 string_literal_length = 0;
483
484 var dummy_token = Token{
485 .start = self.index,
486 .end = self.index,
487 .line_number = self.line_handler.line_number,
488 .id = .invalid,
489 };
490 string_literal_column = dummy_token.calculateColumn(self.buffer, 8, null);
491 },
492 '+', '&', '|' => {
493 self.index += 1;
494 result.id = .operator;
495 self.at_start_of_line = false;
496 break;
497 },
498 '-' => {
499 if (context == .expect_operator) {
500 self.index += 1;
501 result.id = .operator;
502 self.at_start_of_line = false;
503 break;
504 } else {
505 state = .number_literal;
506 still_could_have_exponent = true;
507 exponent_index = null;
508 self.at_start_of_line = false;
509 }
510 },
511 '0'...'9', '~' => {
512 state = .number_literal;
513 still_could_have_exponent = true;
514 exponent_index = null;
515 self.at_start_of_line = false;
516 },
517 '#' => {
518 if (self.at_start_of_line) {
519 state = .preprocessor;
520 } else {
521 state = .literal;
522 }
523 self.at_start_of_line = false;
524 },
525 ';' => {
526 state = .semicolon;
527 self.at_start_of_line = false;
528 },
529 '{', '}' => {
530 self.index += 1;
531 result.id = if (c == '{') .begin else .end;
532 self.at_start_of_line = false;
533 break;
534 },
535 '(', ')' => {
536 self.index += 1;
537 result.id = if (c == '(') .open_paren else .close_paren;
538 self.at_start_of_line = false;
539 break;
540 },
541 ',' => {
542 self.index += 1;
543 result.id = .comma;
544 self.at_start_of_line = false;
545 break;
546 },
547 else => {
548 if (isNonAsciiDigit(c)) {
549 self.error_context_token = .{
550 .id = .number,
551 .start = result.start,
552 .end = self.index + 1,
553 .line_number = self.line_handler.line_number,
554 };
555 return error.InvalidDigitCharacterInNumberLiteral;
556 }
557 state = .literal;
558 self.at_start_of_line = false;
559 },
560 },
561 .preprocessor => switch (c) {
562 '\r', '\n' => {
563 try self.evaluatePreprocessorCommand(result.start, self.index);
564 result.start = self.index + 1;
565 state = .start;
566 result.line_number = self.incrementLineNumber();
567 },
568 else => {},
569 },
570 // Semi-colon acts as a line-terminator--everything is skipped until
571 // the next line.
572 .semicolon => switch (c) {
573 '\r', '\n' => {
574 result.start = self.index + 1;
575 state = .start;
576 result.line_number = self.incrementLineNumber();
577 },
578 else => {},
579 },
580 .number_literal => switch (c) {
581 // zig fmt: off
582 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
583 '\r', '\n', '"', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
584 '\'', ';', '=',
585 => {
586 // zig fmt: on
587 result.id = .number;
588 break;
589 },
590 '0'...'9' => {
591 if (exponent_index) |exp_i| {
592 if (self.index - 1 == exp_i) {
593 // Note: This being an error is a quirk of the preprocessor used by
594 // the Win32 RC compiler.
595 self.error_context_token = .{
596 .id = .number,
597 .start = result.start,
598 .end = self.index + 1,
599 .line_number = self.line_handler.line_number,
600 };
601 return error.InvalidNumberWithExponent;
602 }
603 }
604 },
605 'e', 'E' => {
606 if (still_could_have_exponent) {
607 exponent_index = self.index;
608 still_could_have_exponent = false;
609 }
610 },
611 else => {
612 if (isNonAsciiDigit(c)) {
613 self.error_context_token = .{
614 .id = .number,
615 .start = result.start,
616 .end = self.index + 1,
617 .line_number = self.line_handler.line_number,
618 };
619 return error.InvalidDigitCharacterInNumberLiteral;
620 }
621 still_could_have_exponent = false;
622 },
623 },
624 .literal_or_quoted_wide_string => switch (c) {
625 // zig fmt: off
626 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
627 '\r', '\n', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
628 '\'', ';', '=',
629 // zig fmt: on
630 => {
631 result.id = .literal;
632 break;
633 },
634 '"' => {
635 state = .quoted_wide_string;
636 string_literal_collapsing_whitespace = false;
637 string_literal_length = 0;
638
639 var dummy_token = Token{
640 .start = self.index,
641 .end = self.index,
642 .line_number = self.line_handler.line_number,
643 .id = .invalid,
644 };
645 string_literal_column = dummy_token.calculateColumn(self.buffer, 8, null);
646 },
647 else => {
648 state = .literal;
649 },
650 },
651 .literal => switch (c) {
652 // zig fmt: off
653 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
654 '\r', '\n', '"', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
655 '\'', ';', '=',
656 => {
657 // zig fmt: on
658 result.id = .literal;
659 break;
660 },
661 else => {},
662 },
663 .e => switch (c) {
664 'N', 'n' => {
665 state = .en;
666 },
667 else => {
668 state = .literal;
669 self.index -= 1;
670 },
671 },
672 .en => switch (c) {
673 'D', 'd' => {
674 result.id = .end;
675 self.index += 1;
676 break;
677 },
678 else => {
679 state = .literal;
680 self.index -= 1;
681 },
682 },
683 .b => switch (c) {
684 'E', 'e' => {
685 state = .be;
686 },
687 else => {
688 state = .literal;
689 self.index -= 1;
690 },
691 },
692 .be => switch (c) {
693 'G', 'g' => {
694 state = .beg;
695 },
696 else => {
697 state = .literal;
698 self.index -= 1;
699 },
700 },
701 .beg => switch (c) {
702 'I', 'i' => {
703 state = .begi;
704 },
705 else => {
706 state = .literal;
707 self.index -= 1;
708 },
709 },
710 .begi => switch (c) {
711 'N', 'n' => {
712 result.id = .begin;
713 self.index += 1;
714 break;
715 },
716 else => {
717 state = .literal;
718 self.index -= 1;
719 },
720 },
721 .quoted_ascii_string, .quoted_wide_string => switch (c) {
722 '"' => {
723 string_literal_column += 1;
724 state = if (state == .quoted_ascii_string) .quoted_ascii_string_maybe_end else .quoted_wide_string_maybe_end;
725 },
726 '\\' => {
727 string_literal_length += 1;
728 string_literal_column += 1;
729 state = if (state == .quoted_ascii_string) .quoted_ascii_string_escape else .quoted_wide_string_escape;
730 },
731 '\r' => {
732 string_literal_column = 0;
733 // \r doesn't count towards string literal length
734
735 // Increment line number but don't affect the result token's line number
736 _ = self.incrementLineNumber();
737 },
738 '\n' => {
739 string_literal_column = 0;
740 // first \n expands to <space><\n>
741 if (!string_literal_collapsing_whitespace) {
742 string_literal_length += 2;
743 string_literal_collapsing_whitespace = true;
744 }
745 // the rest are collapsed into the <space><\n>
746
747 // Increment line number but don't affect the result token's line number
748 _ = self.incrementLineNumber();
749 },
750 // only \t, space, Vertical Tab, and Form Feed count as whitespace when collapsing
751 '\t', ' ', '\x0b', '\x0c' => {
752 if (!string_literal_collapsing_whitespace) {
753 // Literal tab characters are counted as the number of space characters
754 // needed to reach the next 8-column tab stop.
755 const width = columnWidth(string_literal_column, @intCast(c), 8);
756 string_literal_length += width;
757 string_literal_column += width;
758 }
759 },
760 else => {
761 string_literal_collapsing_whitespace = false;
762 string_literal_length += 1;
763 string_literal_column += 1;
764 },
765 },
766 .quoted_ascii_string_escape, .quoted_wide_string_escape => switch (c) {
767 '"' => {
768 self.error_context_token = .{
769 .id = .invalid,
770 .start = self.index - 1,
771 .end = self.index + 1,
772 .line_number = self.line_handler.line_number,
773 };
774 return error.FoundCStyleEscapedQuote;
775 },
776 else => {
777 string_literal_length += 1;
778 string_literal_column += 1;
779 state = if (state == .quoted_ascii_string_escape) .quoted_ascii_string else .quoted_wide_string;
780 },
781 },
782 .quoted_ascii_string_maybe_end, .quoted_wide_string_maybe_end => switch (c) {
783 '"' => {
784 state = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
785 // Escaped quotes count as 1 char for string literal length checks.
786 // Since we did not increment on the first " (because it could have been
787 // the end of the quoted string), we increment here
788 string_literal_length += 1;
789 string_literal_column += 1;
790 },
791 else => {
792 result.id = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
793 break;
794 },
795 },
796 }
797 } else { // got EOF
798 switch (state) {
799 .start, .semicolon => {},
800 .literal_or_quoted_wide_string, .literal, .e, .en, .b, .be, .beg, .begi => {
801 result.id = .literal;
802 },
803 .preprocessor => {
804 try self.evaluatePreprocessorCommand(result.start, self.index);
805 result.start = self.index;
806 },
807 .number_literal => {
808 result.id = .number;
809 },
810 .quoted_ascii_string_maybe_end, .quoted_wide_string_maybe_end => {
811 result.id = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
812 },
813 .quoted_ascii_string,
814 .quoted_wide_string,
815 .quoted_ascii_string_escape,
816 .quoted_wide_string_escape,
817 => {
818 self.error_context_token = .{
819 .id = .eof,
820 .start = self.index,
821 .end = self.index,
822 .line_number = self.line_handler.line_number,
823 };
824 return LexError.UnfinishedStringLiteral;
825 },
826 }
827 }
828
829 result.end = self.index;
830
831 if (result.id == .quoted_ascii_string or result.id == .quoted_wide_string) {
832 if (string_literal_length > self.max_string_literal_codepoints) {
833 self.error_context_token = result;
834 return LexError.StringLiteralTooLong;
835 }
836 }
837
838 return result;
839 }
840
841 /// Increments line_number appropriately (handling line ending pairs)
842 /// and returns the new line number.
843 fn incrementLineNumber(self: *Self) usize {
844 _ = self.line_handler.incrementLineNumber(self.index);
845 self.at_start_of_line = true;
846 return self.line_handler.line_number;
847 }
848
849 fn checkForIllegalCodepoint(self: *Self, codepoint: code_pages.Codepoint, in_string_literal: bool) LexError!void {
850 const err = switch (codepoint.value) {
851 // 0x00 = NUL
852 // 0x1A = Substitute (treated as EOF)
853 // NOTE: 0x1A gets treated as EOF by the clang preprocessor so after a .rc file
854 // is run through the clang preprocessor it will no longer have 0x1A characters in it.
855 // 0x7F = DEL (treated as a context-specific terminator by the Windows RC compiler)
856 0x00, 0x1A, 0x7F => error.IllegalByte,
857 // 0x01...0x03 result in strange 'macro definition too big' errors when used outside of string literals
858 // 0x04 is valid but behaves strangely (sort of acts as a 'skip the next character' instruction)
859 0x01...0x04 => if (!in_string_literal) error.IllegalByteOutsideStringLiterals else return,
860 // @ and ` both result in error RC2018: unknown character '0x60' (and subsequently
861 // fatal error RC1116: RC terminating after preprocessor errors) if they are ever used
862 // outside of string literals. Not exactly sure why this would be the case, though.
863 // TODO: Make sure there aren't any exceptions
864 '@', '`' => if (!in_string_literal) error.IllegalByteOutsideStringLiterals else return,
865 // The Byte Order Mark is mostly skipped over by the Windows RC compiler, but
866 // there are edge cases where it leads to cryptic 'compiler limit : macro definition too big'
867 // errors (e.g. a BOM within a number literal). By making this illegal we avoid having to
868 // deal with a lot of edge cases and remove the potential footgun of the bytes of a BOM
869 // being 'missing' when included in a string literal (the Windows RC compiler acts as
870 // if the codepoint was never part of the string literal).
871 '\u{FEFF}' => error.IllegalByteOrderMark,
872 // Similar deal with this private use codepoint, it gets skipped/ignored by the
873 // RC compiler (but without the cryptic errors). Silently dropping bytes still seems like
874 // enough of a footgun with no real use-cases that it's still worth erroring instead of
875 // emulating the RC compiler's behavior, though.
876 '\u{E000}' => error.IllegalPrivateUseCharacter,
877 // These codepoints lead to strange errors when used outside of string literals,
878 // and miscompilations when used within string literals. We avoid the miscompilation
879 // within string literals and emit a warning, but outside of string literals it makes
880 // more sense to just disallow these codepoints.
881 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => if (!in_string_literal) error.IllegalCodepointOutsideStringLiterals else return,
882 else => return,
883 };
884 self.error_context_token = .{
885 .id = .invalid,
886 .start = self.index,
887 .end = self.index + codepoint.byte_len,
888 .line_number = self.line_handler.line_number,
889 };
890 return err;
891 }
892
893 fn evaluatePreprocessorCommand(self: *Self, start: usize, end: usize) !void {
894 const token = Token{
895 .id = .preprocessor_command,
896 .start = start,
897 .end = end,
898 .line_number = self.line_handler.line_number,
899 };
900 errdefer self.error_context_token = token;
901 const full_command = self.buffer[start..end];
902 var command = full_command;
903
904 // Anything besides exactly this is ignored by the Windows RC implementation
905 const expected_directive = "#pragma";
906 if (!std.mem.startsWith(u8, command, expected_directive)) return;
907 command = command[expected_directive.len..];
908
909 if (command.len == 0 or !std.ascii.isWhitespace(command[0])) return;
910 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
911 command = command[1..];
912 }
913
914 // Note: CoDe_PaGeZ is also treated as "code_page" by the Windows RC implementation,
915 // and it will error with 'Missing left parenthesis in code_page #pragma'
916 const expected_extension = "code_page";
917 if (!std.ascii.startsWithIgnoreCase(command, expected_extension)) return;
918 command = command[expected_extension.len..];
919
920 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
921 command = command[1..];
922 }
923
924 if (command.len == 0 or command[0] != '(') {
925 return error.CodePagePragmaMissingLeftParen;
926 }
927 command = command[1..];
928
929 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
930 command = command[1..];
931 }
932
933 var num_str: []u8 = command[0..0];
934 while (command.len > 0 and (command[0] != ')' and !std.ascii.isWhitespace(command[0]))) {
935 command = command[1..];
936 num_str.len += 1;
937 }
938
939 if (num_str.len == 0) {
940 return error.CodePagePragmaNotInteger;
941 }
942
943 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
944 command = command[1..];
945 }
946
947 if (command.len == 0 or command[0] != ')') {
948 return error.CodePagePragmaMissingRightParen;
949 }
950
951 const code_page = code_page: {
952 if (std.ascii.eqlIgnoreCase("DEFAULT", num_str)) {
953 break :code_page self.default_code_page;
954 }
955
956 // The Win32 compiler behaves fairly strangely around maxInt(u32):
957 // - If the overflowed u32 wraps and becomes a known code page ID, then
958 // it will error/warn with "Codepage not valid: ignored" (depending on /w)
959 // - If the overflowed u32 wraps and does not become a known code page ID,
960 // then it will error with 'constant too big' and 'Codepage not integer'
961 //
962 // Instead of that, we just have a separate error specifically for overflow.
963 const num = parseCodePageNum(num_str) catch |err| switch (err) {
964 error.InvalidCharacter => return error.CodePagePragmaNotInteger,
965 error.Overflow => return error.CodePagePragmaOverflow,
966 };
967
968 // Anything that starts with 0 but does not resolve to 0 is treated as invalid, e.g. 01252
969 if (num_str[0] == '0' and num != 0) {
970 return error.CodePagePragmaInvalidCodePage;
971 }
972 // Anything that resolves to 0 is treated as 'not an integer' by the Win32 implementation.
973 else if (num == 0) {
974 return error.CodePagePragmaNotInteger;
975 }
976 // Anything above u16 max is not going to be found since our CodePage enum is backed by a u16.
977 if (num > std.math.maxInt(u16)) {
978 return error.CodePagePragmaInvalidCodePage;
979 }
980
981 break :code_page code_pages.CodePage.getByIdentifierEnsureSupported(@intCast(num)) catch |err| switch (err) {
982 error.InvalidCodePage => return error.CodePagePragmaInvalidCodePage,
983 error.UnsupportedCodePage => return error.CodePagePragmaUnsupportedCodePage,
984 };
985 };
986
987 // https://learn.microsoft.com/en-us/windows/win32/menurc/pragma-directives
988 // > This pragma is not supported in an included resource file (.rc)
989 //
990 // Even though the Win32 behavior is to just ignore such directives silently,
991 // this is an error in the lexer to allow for emitting warnings/errors when
992 // such directives are found if that's wanted. The intention is for the lexer
993 // to still be able to work correctly after this error is returned.
994 if (self.source_mappings) |source_mappings| {
995 if (!source_mappings.isRootFile(token.line_number)) {
996 return error.CodePagePragmaInIncludedFile;
997 }
998 }
999
1000 self.seen_pragma_code_pages +|= 1;
1001 self.current_code_page = code_page;
1002 }
1003
1004 fn parseCodePageNum(str: []const u8) !u32 {
1005 var x: u32 = 0;
1006 for (str) |c| {
1007 const digit = try std.fmt.charToDigit(c, 10);
1008 if (x != 0) x = try std.math.mul(u32, x, 10);
1009 x = try std.math.add(u32, x, digit);
1010 }
1011 return x;
1012 }
1013
1014 pub fn getErrorDetails(self: Self, lex_err: LexError) ErrorDetails {
1015 const err = switch (lex_err) {
1016 error.UnfinishedStringLiteral => ErrorDetails.Error.unfinished_string_literal,
1017 error.StringLiteralTooLong => return .{
1018 .err = .string_literal_too_long,
1019 .token = self.error_context_token.?,
1020 .extra = .{ .number = self.max_string_literal_codepoints },
1021 },
1022 error.InvalidNumberWithExponent => ErrorDetails.Error.invalid_number_with_exponent,
1023 error.InvalidDigitCharacterInNumberLiteral => ErrorDetails.Error.invalid_digit_character_in_number_literal,
1024 error.IllegalByte => ErrorDetails.Error.illegal_byte,
1025 error.IllegalByteOutsideStringLiterals => ErrorDetails.Error.illegal_byte_outside_string_literals,
1026 error.IllegalCodepointOutsideStringLiterals => ErrorDetails.Error.illegal_codepoint_outside_string_literals,
1027 error.IllegalByteOrderMark => ErrorDetails.Error.illegal_byte_order_mark,
1028 error.IllegalPrivateUseCharacter => ErrorDetails.Error.illegal_private_use_character,
1029 error.FoundCStyleEscapedQuote => ErrorDetails.Error.found_c_style_escaped_quote,
1030 error.CodePagePragmaMissingLeftParen => ErrorDetails.Error.code_page_pragma_missing_left_paren,
1031 error.CodePagePragmaMissingRightParen => ErrorDetails.Error.code_page_pragma_missing_right_paren,
1032 error.CodePagePragmaInvalidCodePage => ErrorDetails.Error.code_page_pragma_invalid_code_page,
1033 error.CodePagePragmaNotInteger => ErrorDetails.Error.code_page_pragma_not_integer,
1034 error.CodePagePragmaOverflow => ErrorDetails.Error.code_page_pragma_overflow,
1035 error.CodePagePragmaUnsupportedCodePage => ErrorDetails.Error.code_page_pragma_unsupported_code_page,
1036 error.CodePagePragmaInIncludedFile => ErrorDetails.Error.code_page_pragma_in_included_file,
1037 };
1038 return .{
1039 .err = err,
1040 .token = self.error_context_token.?,
1041 };
1042 }
1043};
1044
1045fn testLexNormal(source: []const u8, expected_tokens: []const Token.Id) !void {
1046 var lexer = Lexer.init(source, .{});
1047 if (dumpTokensDuringTests) std.debug.print("\n----------------------\n{s}\n----------------------\n", .{lexer.buffer});
1048 for (expected_tokens) |expected_token_id| {
1049 const token = try lexer.nextNormal();
1050 if (dumpTokensDuringTests) lexer.dump(&token);
1051 try std.testing.expectEqual(expected_token_id, token.id);
1052 }
1053 const last_token = try lexer.nextNormal();
1054 try std.testing.expectEqual(Token.Id.eof, last_token.id);
1055}
1056
1057fn expectLexError(expected: LexError, actual: anytype) !void {
1058 try std.testing.expectError(expected, actual);
1059 if (dumpTokensDuringTests) std.debug.print("{!}\n", .{actual});
1060}
1061
1062test "normal: numbers" {
1063 try testLexNormal("1", &.{.number});
1064 try testLexNormal("-1", &.{.number});
1065 try testLexNormal("- 1", &.{ .number, .number });
1066 try testLexNormal("-a", &.{.number});
1067}
1068
1069test "normal: string literals" {
1070 try testLexNormal("\"\"", &.{.quoted_ascii_string});
1071 // "" is an escaped "
1072 try testLexNormal("\" \"\" \"", &.{.quoted_ascii_string});
1073}
1074
1075test "superscript chars and code pages" {
1076 const firstToken = struct {
1077 pub fn firstToken(source: []const u8, default_code_page: CodePage, comptime lex_method: Lexer.LexMethod) LexError!Token {
1078 var lexer = Lexer.init(source, .{ .default_code_page = default_code_page });
1079 return lexer.next(lex_method);
1080 }
1081 }.firstToken;
1082 const utf8_source = "²";
1083 const windows1252_source = "\xB2";
1084
1085 const windows1252_encoded_as_windows1252 = firstToken(windows1252_source, .windows1252, .normal);
1086 try std.testing.expectError(error.InvalidDigitCharacterInNumberLiteral, windows1252_encoded_as_windows1252);
1087
1088 const utf8_encoded_as_windows1252 = try firstToken(utf8_source, .windows1252, .normal);
1089 try std.testing.expectEqual(Token{
1090 .id = .literal,
1091 .start = 0,
1092 .end = 2,
1093 .line_number = 1,
1094 }, utf8_encoded_as_windows1252);
1095
1096 const utf8_encoded_as_utf8 = firstToken(utf8_source, .utf8, .normal);
1097 try std.testing.expectError(error.InvalidDigitCharacterInNumberLiteral, utf8_encoded_as_utf8);
1098
1099 const windows1252_encoded_as_utf8 = try firstToken(windows1252_source, .utf8, .normal);
1100 try std.testing.expectEqual(Token{
1101 .id = .literal,
1102 .start = 0,
1103 .end = 1,
1104 .line_number = 1,
1105 }, windows1252_encoded_as_utf8);
1106}
lib/compiler/resinator/literals.zig created+910
...@@ -0,0 +1,910 @@
1const std = @import("std");
2const code_pages = @import("code_pages.zig");
3const CodePage = code_pages.CodePage;
4const windows1252 = @import("windows1252.zig");
5const ErrorDetails = @import("errors.zig").ErrorDetails;
6const DiagnosticsContext = @import("errors.zig").DiagnosticsContext;
7const Token = @import("lex.zig").Token;
8
9/// rc is maximally liberal in terms of what it accepts as a number literal
10/// for data values. As long as it starts with a number or - or ~, that's good enough.
11pub fn isValidNumberDataLiteral(str: []const u8) bool {
12 if (str.len == 0) return false;
13 switch (str[0]) {
14 '~', '-', '0'...'9' => return true,
15 else => return false,
16 }
17}
18
19pub const SourceBytes = struct {
20 slice: []const u8,
21 code_page: CodePage,
22};
23
24pub const StringType = enum { ascii, wide };
25
26/// Valid escapes:
27/// "" -> "
28/// \a, \A => 0x08 (not 0x07 like in C)
29/// \n => 0x0A
30/// \r => 0x0D
31/// \t, \T => 0x09
32/// \\ => \
33/// \nnn => byte with numeric value given by nnn interpreted as octal
34/// (wraps on overflow, number of digits can be 1-3 for ASCII strings
35/// and 1-7 for wide strings)
36/// \xhh => byte with numeric value given by hh interpreted as hex
37/// (number of digits can be 0-2 for ASCII strings and 0-4 for
38/// wide strings)
39/// \<\r+> => \
40/// \<[\r\n\t ]+> => <nothing>
41///
42/// Special cases:
43/// <\t> => 1-8 spaces, dependent on columns in the source rc file itself
44/// <\r> => <nothing>
45/// <\n+><\w+?\n?> => <space><\n>
46///
47/// Special, especially weird case:
48/// \"" => "
49/// NOTE: This leads to footguns because the preprocessor can start parsing things
50/// out-of-sync with the RC compiler, expanding macros within string literals, etc.
51/// This parse function handles this case the same as the Windows RC compiler, but
52/// \" within a string literal is treated as an error by the lexer, so the relevant
53/// branches should never actually be hit during this function.
54pub const IterativeStringParser = struct {
55 source: []const u8,
56 code_page: CodePage,
57 /// The type of the string inferred by the prefix (L"" or "")
58 /// This is what matters for things like the maximum digits in an
59 /// escape sequence, whether or not invalid escape sequences are skipped, etc.
60 declared_string_type: StringType,
61 pending_codepoint: ?u21 = null,
62 num_pending_spaces: u8 = 0,
63 index: usize = 0,
64 column: usize = 0,
65 diagnostics: ?DiagnosticsContext = null,
66 seen_tab: bool = false,
67
68 const State = enum {
69 normal,
70 quote,
71 newline,
72 escaped,
73 escaped_cr,
74 escaped_newlines,
75 escaped_octal,
76 escaped_hex,
77 };
78
79 pub fn init(bytes: SourceBytes, options: StringParseOptions) IterativeStringParser {
80 const declared_string_type: StringType = switch (bytes.slice[0]) {
81 'L', 'l' => .wide,
82 else => .ascii,
83 };
84 var source = bytes.slice[1 .. bytes.slice.len - 1]; // remove ""
85 var column = options.start_column + 1; // for the removed "
86 if (declared_string_type == .wide) {
87 source = source[1..]; // remove L
88 column += 1; // for the removed L
89 }
90 return .{
91 .source = source,
92 .code_page = bytes.code_page,
93 .declared_string_type = declared_string_type,
94 .column = column,
95 .diagnostics = options.diagnostics,
96 };
97 }
98
99 pub const ParsedCodepoint = struct {
100 codepoint: u21,
101 /// Note: If this is true, `codepoint` will be a value with a max of maxInt(u16).
102 /// This is enforced by using saturating arithmetic, so in e.g. a wide string literal the
103 /// octal escape sequence \7777777 (2,097,151) will be parsed into the value 0xFFFF (65,535).
104 /// If the value needs to be truncated to a smaller integer (for ASCII string literals), then that
105 /// must be done by the caller.
106 from_escaped_integer: bool = false,
107 };
108
109 pub fn next(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
110 const result = try self.nextUnchecked();
111 if (self.diagnostics != null and result != null and !result.?.from_escaped_integer) {
112 switch (result.?.codepoint) {
113 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => {
114 const err: ErrorDetails.Error = if (result.?.codepoint == 0xD00)
115 .rc_would_miscompile_codepoint_skip
116 else
117 .rc_would_miscompile_codepoint_byte_swap;
118 try self.diagnostics.?.diagnostics.append(ErrorDetails{
119 .err = err,
120 .type = .warning,
121 .token = self.diagnostics.?.token,
122 .extra = .{ .number = result.?.codepoint },
123 });
124 try self.diagnostics.?.diagnostics.append(ErrorDetails{
125 .err = err,
126 .type = .note,
127 .token = self.diagnostics.?.token,
128 .print_source_line = false,
129 .extra = .{ .number = result.?.codepoint },
130 });
131 },
132 else => {},
133 }
134 }
135 return result;
136 }
137
138 pub fn nextUnchecked(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
139 if (self.num_pending_spaces > 0) {
140 // Ensure that we don't get into this predicament so we can ensure that
141 // the order of processing any pending stuff doesn't matter
142 std.debug.assert(self.pending_codepoint == null);
143 self.num_pending_spaces -= 1;
144 return .{ .codepoint = ' ' };
145 }
146 if (self.pending_codepoint) |pending_codepoint| {
147 self.pending_codepoint = null;
148 return .{ .codepoint = pending_codepoint };
149 }
150 if (self.index >= self.source.len) return null;
151
152 var state: State = .normal;
153 var string_escape_n: u16 = 0;
154 var string_escape_i: u8 = 0;
155 const max_octal_escape_digits: u8 = switch (self.declared_string_type) {
156 .ascii => 3,
157 .wide => 7,
158 };
159 const max_hex_escape_digits: u8 = switch (self.declared_string_type) {
160 .ascii => 2,
161 .wide => 4,
162 };
163
164 var backtrack: bool = undefined;
165 while (self.code_page.codepointAt(self.index, self.source)) |codepoint| : ({
166 if (!backtrack) self.index += codepoint.byte_len;
167 }) {
168 backtrack = false;
169 const c = codepoint.value;
170 defer {
171 if (!backtrack) {
172 if (c == '\t') {
173 self.column += columnsUntilTabStop(self.column, 8);
174 } else {
175 self.column += codepoint.byte_len;
176 }
177 }
178 }
179 switch (state) {
180 .normal => switch (c) {
181 '\\' => state = .escaped,
182 '"' => state = .quote,
183 '\r' => {},
184 '\n' => state = .newline,
185 '\t' => {
186 // Only warn about a tab getting converted to spaces once per string
187 if (self.diagnostics != null and !self.seen_tab) {
188 try self.diagnostics.?.diagnostics.append(ErrorDetails{
189 .err = .tab_converted_to_spaces,
190 .type = .warning,
191 .token = self.diagnostics.?.token,
192 });
193 try self.diagnostics.?.diagnostics.append(ErrorDetails{
194 .err = .tab_converted_to_spaces,
195 .type = .note,
196 .token = self.diagnostics.?.token,
197 .print_source_line = false,
198 });
199 self.seen_tab = true;
200 }
201 const cols = columnsUntilTabStop(self.column, 8);
202 self.num_pending_spaces = @intCast(cols - 1);
203 self.index += codepoint.byte_len;
204 return .{ .codepoint = ' ' };
205 },
206 else => {
207 self.index += codepoint.byte_len;
208 return .{ .codepoint = c };
209 },
210 },
211 .quote => switch (c) {
212 '"' => {
213 // "" => "
214 self.index += codepoint.byte_len;
215 return .{ .codepoint = '"' };
216 },
217 else => unreachable, // this is a bug in the lexer
218 },
219 .newline => switch (c) {
220 '\r', ' ', '\t', '\n', '\x0b', '\x0c', '\xa0' => {},
221 else => {
222 // we intentionally avoid incrementing self.index
223 // to handle the current char in the next call,
224 // and we set backtrack so column count is handled correctly
225 backtrack = true;
226
227 // <space><newline>
228 self.pending_codepoint = '\n';
229 return .{ .codepoint = ' ' };
230 },
231 },
232 .escaped => switch (c) {
233 '\r' => state = .escaped_cr,
234 '\n' => state = .escaped_newlines,
235 '0'...'7' => {
236 string_escape_n = std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
237 string_escape_i = 1;
238 state = .escaped_octal;
239 },
240 'x', 'X' => {
241 string_escape_n = 0;
242 string_escape_i = 0;
243 state = .escaped_hex;
244 },
245 else => {
246 switch (c) {
247 'a', 'A' => {
248 self.index += codepoint.byte_len;
249 return .{ .codepoint = '\x08' };
250 }, // might be a bug in RC, but matches its behavior
251 'n' => {
252 self.index += codepoint.byte_len;
253 return .{ .codepoint = '\n' };
254 },
255 'r' => {
256 self.index += codepoint.byte_len;
257 return .{ .codepoint = '\r' };
258 },
259 't', 'T' => {
260 self.index += codepoint.byte_len;
261 return .{ .codepoint = '\t' };
262 },
263 '\\' => {
264 self.index += codepoint.byte_len;
265 return .{ .codepoint = '\\' };
266 },
267 '"' => {
268 // \" is a special case that doesn't get the \ included,
269 backtrack = true;
270 },
271 else => switch (self.declared_string_type) {
272 .wide => {}, // invalid escape sequences are skipped in wide strings
273 .ascii => {
274 // we intentionally avoid incrementing self.index
275 // to handle the current char in the next call,
276 // and we set backtrack so column count is handled correctly
277 backtrack = true;
278 return .{ .codepoint = '\\' };
279 },
280 },
281 }
282 state = .normal;
283 },
284 },
285 .escaped_cr => switch (c) {
286 '\r' => {},
287 '\n' => state = .escaped_newlines,
288 else => {
289 // we intentionally avoid incrementing self.index
290 // to handle the current char in the next call,
291 // and we set backtrack so column count is handled correctly
292 backtrack = true;
293 return .{ .codepoint = '\\' };
294 },
295 },
296 .escaped_newlines => switch (c) {
297 '\r', '\n', '\t', ' ', '\x0b', '\x0c', '\xa0' => {},
298 else => {
299 // backtrack so that we handle the current char properly
300 backtrack = true;
301 state = .normal;
302 },
303 },
304 .escaped_octal => switch (c) {
305 '0'...'7' => {
306 string_escape_n *%= 8;
307 string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
308 string_escape_i += 1;
309 if (string_escape_i == max_octal_escape_digits) {
310 self.index += codepoint.byte_len;
311 return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
312 }
313 },
314 else => {
315 // we intentionally avoid incrementing self.index
316 // to handle the current char in the next call,
317 // and we set backtrack so column count is handled correctly
318 backtrack = true;
319
320 // write out whatever byte we have parsed so far
321 return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
322 },
323 },
324 .escaped_hex => switch (c) {
325 '0'...'9', 'a'...'f', 'A'...'F' => {
326 string_escape_n *= 16;
327 string_escape_n += std.fmt.charToDigit(@intCast(c), 16) catch unreachable;
328 string_escape_i += 1;
329 if (string_escape_i == max_hex_escape_digits) {
330 self.index += codepoint.byte_len;
331 return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
332 }
333 },
334 else => {
335 // we intentionally avoid incrementing self.index
336 // to handle the current char in the next call,
337 // and we set backtrack so column count is handled correctly
338 backtrack = true;
339
340 // write out whatever byte we have parsed so far
341 // (even with 0 actual digits, \x alone parses to 0)
342 const escaped_value = string_escape_n;
343 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
344 },
345 },
346 }
347 }
348
349 switch (state) {
350 .normal, .escaped_newlines => {},
351 .newline => {
352 // <space><newline>
353 self.pending_codepoint = '\n';
354 return .{ .codepoint = ' ' };
355 },
356 .escaped, .escaped_cr => return .{ .codepoint = '\\' },
357 .escaped_octal, .escaped_hex => {
358 return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
359 },
360 .quote => unreachable, // this is a bug in the lexer
361 }
362
363 return null;
364 }
365};
366
367pub const StringParseOptions = struct {
368 start_column: usize = 0,
369 diagnostics: ?DiagnosticsContext = null,
370 output_code_page: CodePage = .windows1252,
371};
372
373pub fn parseQuotedString(
374 comptime literal_type: StringType,
375 allocator: std.mem.Allocator,
376 bytes: SourceBytes,
377 options: StringParseOptions,
378) !(switch (literal_type) {
379 .ascii => []u8,
380 .wide => [:0]u16,
381}) {
382 const T = if (literal_type == .ascii) u8 else u16;
383 std.debug.assert(bytes.slice.len >= 2); // must at least have 2 double quote chars
384
385 var buf = try std.ArrayList(T).initCapacity(allocator, bytes.slice.len);
386 errdefer buf.deinit();
387
388 var iterative_parser = IterativeStringParser.init(bytes, options);
389
390 while (try iterative_parser.next()) |parsed| {
391 const c = parsed.codepoint;
392 if (parsed.from_escaped_integer) {
393 // We truncate here to get the correct behavior for ascii strings
394 try buf.append(std.mem.nativeToLittle(T, @truncate(c)));
395 } else {
396 switch (literal_type) {
397 .ascii => switch (options.output_code_page) {
398 .windows1252 => {
399 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
400 try buf.append(best_fit);
401 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
402 try buf.append('?');
403 } else {
404 try buf.appendSlice("??");
405 }
406 },
407 .utf8 => {
408 var codepoint_to_encode = c;
409 if (c == code_pages.Codepoint.invalid) {
410 codepoint_to_encode = '�';
411 }
412 var utf8_buf: [4]u8 = undefined;
413 const utf8_len = std.unicode.utf8Encode(codepoint_to_encode, &utf8_buf) catch unreachable;
414 try buf.appendSlice(utf8_buf[0..utf8_len]);
415 },
416 else => unreachable, // Unsupported code page
417 },
418 .wide => {
419 if (c == code_pages.Codepoint.invalid) {
420 try buf.append(std.mem.nativeToLittle(u16, '�'));
421 } else if (c < 0x10000) {
422 const short: u16 = @intCast(c);
423 try buf.append(std.mem.nativeToLittle(u16, short));
424 } else {
425 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
426 try buf.append(std.mem.nativeToLittle(u16, high));
427 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
428 try buf.append(std.mem.nativeToLittle(u16, low));
429 }
430 },
431 }
432 }
433 }
434
435 if (literal_type == .wide) {
436 return buf.toOwnedSliceSentinel(0);
437 } else {
438 return buf.toOwnedSlice();
439 }
440}
441
442pub fn parseQuotedAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 {
443 std.debug.assert(bytes.slice.len >= 2); // ""
444 return parseQuotedString(.ascii, allocator, bytes, options);
445}
446
447pub fn parseQuotedWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {
448 std.debug.assert(bytes.slice.len >= 3); // L""
449 return parseQuotedString(.wide, allocator, bytes, options);
450}
451
452pub fn parseQuotedStringAsWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {
453 std.debug.assert(bytes.slice.len >= 2); // ""
454 return parseQuotedString(.wide, allocator, bytes, options);
455}
456
457test "parse quoted ascii string" {
458 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
459 defer arena_allocator.deinit();
460 const arena = arena_allocator.allocator();
461
462 try std.testing.expectEqualSlices(u8, "hello", try parseQuotedAsciiString(arena, .{
463 .slice =
464 \\"hello"
465 ,
466 .code_page = .windows1252,
467 }, .{}));
468 // hex with 0 digits
469 try std.testing.expectEqualSlices(u8, "\x00", try parseQuotedAsciiString(arena, .{
470 .slice =
471 \\"\x"
472 ,
473 .code_page = .windows1252,
474 }, .{}));
475 // hex max of 2 digits
476 try std.testing.expectEqualSlices(u8, "\xFFf", try parseQuotedAsciiString(arena, .{
477 .slice =
478 \\"\XfFf"
479 ,
480 .code_page = .windows1252,
481 }, .{}));
482 // octal with invalid octal digit
483 try std.testing.expectEqualSlices(u8, "\x019", try parseQuotedAsciiString(arena, .{
484 .slice =
485 \\"\19"
486 ,
487 .code_page = .windows1252,
488 }, .{}));
489 // escaped quotes
490 try std.testing.expectEqualSlices(u8, " \" ", try parseQuotedAsciiString(arena, .{
491 .slice =
492 \\" "" "
493 ,
494 .code_page = .windows1252,
495 }, .{}));
496 // backslash right before escaped quotes
497 try std.testing.expectEqualSlices(u8, "\"", try parseQuotedAsciiString(arena, .{
498 .slice =
499 \\"\"""
500 ,
501 .code_page = .windows1252,
502 }, .{}));
503 // octal overflow
504 try std.testing.expectEqualSlices(u8, "\x01", try parseQuotedAsciiString(arena, .{
505 .slice =
506 \\"\401"
507 ,
508 .code_page = .windows1252,
509 }, .{}));
510 // escapes
511 try std.testing.expectEqualSlices(u8, "\x08\n\r\t\\", try parseQuotedAsciiString(arena, .{
512 .slice =
513 \\"\a\n\r\t\\"
514 ,
515 .code_page = .windows1252,
516 }, .{}));
517 // uppercase escapes
518 try std.testing.expectEqualSlices(u8, "\x08\\N\\R\t\\", try parseQuotedAsciiString(arena, .{
519 .slice =
520 \\"\A\N\R\T\\"
521 ,
522 .code_page = .windows1252,
523 }, .{}));
524 // backslash on its own
525 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(arena, .{
526 .slice =
527 \\"\"
528 ,
529 .code_page = .windows1252,
530 }, .{}));
531 // unrecognized escapes
532 try std.testing.expectEqualSlices(u8, "\\b", try parseQuotedAsciiString(arena, .{
533 .slice =
534 \\"\b"
535 ,
536 .code_page = .windows1252,
537 }, .{}));
538 // escaped carriage returns
539 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(
540 arena,
541 .{ .slice = "\"\\\r\r\r\r\r\"", .code_page = .windows1252 },
542 .{},
543 ));
544 // escaped newlines
545 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
546 arena,
547 .{ .slice = "\"\\\n\n\n\n\n\"", .code_page = .windows1252 },
548 .{},
549 ));
550 // escaped CRLF pairs
551 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
552 arena,
553 .{ .slice = "\"\\\r\n\r\n\r\n\r\n\r\n\"", .code_page = .windows1252 },
554 .{},
555 ));
556 // escaped newlines with other whitespace
557 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
558 arena,
559 .{ .slice = "\"\\\n \t\r\n \r\t\n \t\"", .code_page = .windows1252 },
560 .{},
561 ));
562 // literal tab characters get converted to spaces (dependent on source file columns)
563 try std.testing.expectEqualSlices(u8, " ", try parseQuotedAsciiString(
564 arena,
565 .{ .slice = "\"\t\"", .code_page = .windows1252 },
566 .{},
567 ));
568 try std.testing.expectEqualSlices(u8, "abc ", try parseQuotedAsciiString(
569 arena,
570 .{ .slice = "\"abc\t\"", .code_page = .windows1252 },
571 .{},
572 ));
573 try std.testing.expectEqualSlices(u8, "abcdefg ", try parseQuotedAsciiString(
574 arena,
575 .{ .slice = "\"abcdefg\t\"", .code_page = .windows1252 },
576 .{},
577 ));
578 try std.testing.expectEqualSlices(u8, "\\ ", try parseQuotedAsciiString(
579 arena,
580 .{ .slice = "\"\\\t\"", .code_page = .windows1252 },
581 .{},
582 ));
583 // literal CR's get dropped
584 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
585 arena,
586 .{ .slice = "\"\r\r\r\r\r\"", .code_page = .windows1252 },
587 .{},
588 ));
589 // contiguous newlines and whitespace get collapsed to <space><newline>
590 try std.testing.expectEqualSlices(u8, " \n", try parseQuotedAsciiString(
591 arena,
592 .{ .slice = "\"\n\r\r \r\n \t \"", .code_page = .windows1252 },
593 .{},
594 ));
595}
596
597test "parse quoted ascii string with utf8 code page" {
598 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
599 defer arena_allocator.deinit();
600 const arena = arena_allocator.allocator();
601
602 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
603 arena,
604 .{ .slice = "\"\"", .code_page = .utf8 },
605 .{},
606 ));
607 // Codepoints that don't have a Windows-1252 representation get converted to ?
608 try std.testing.expectEqualSlices(u8, "?????????", try parseQuotedAsciiString(
609 arena,
610 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
611 .{},
612 ));
613 // Codepoints that have a best fit mapping get converted accordingly,
614 // these are box drawing codepoints
615 try std.testing.expectEqualSlices(u8, "\x2b\x2d\x2b", try parseQuotedAsciiString(
616 arena,
617 .{ .slice = "\"┌─┐\"", .code_page = .utf8 },
618 .{},
619 ));
620 // Invalid UTF-8 gets converted to ? depending on well-formedness
621 try std.testing.expectEqualSlices(u8, "????", try parseQuotedAsciiString(
622 arena,
623 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
624 .{},
625 ));
626 // Codepoints that would require a UTF-16 surrogate pair get converted to ??
627 try std.testing.expectEqualSlices(u8, "??", try parseQuotedAsciiString(
628 arena,
629 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
630 .{},
631 ));
632
633 // Output code page changes how invalid UTF-8 gets converted, since it
634 // now encodes the result as UTF-8 so it can write replacement characters.
635 try std.testing.expectEqualSlices(u8, "����", try parseQuotedAsciiString(
636 arena,
637 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
638 .{ .output_code_page = .utf8 },
639 ));
640 try std.testing.expectEqualSlices(u8, "\xF2\xAF\xBA\xB4", try parseQuotedAsciiString(
641 arena,
642 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
643 .{ .output_code_page = .utf8 },
644 ));
645
646 // This used to cause integer overflow when reconsuming the 4-byte long codepoint
647 // after the escaped CRLF pair.
648 try std.testing.expectEqualSlices(u8, "\u{10348}", try parseQuotedAsciiString(
649 arena,
650 .{ .slice = "\"\\\r\n\u{10348}\"", .code_page = .utf8 },
651 .{ .output_code_page = .utf8 },
652 ));
653}
654
655test "parse quoted wide string" {
656 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
657 defer arena_allocator.deinit();
658 const arena = arena_allocator.allocator();
659
660 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("hello"), try parseQuotedWideString(arena, .{
661 .slice =
662 \\L"hello"
663 ,
664 .code_page = .windows1252,
665 }, .{}));
666 // hex with 0 digits
667 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{0x0}, try parseQuotedWideString(arena, .{
668 .slice =
669 \\L"\x"
670 ,
671 .code_page = .windows1252,
672 }, .{}));
673 // hex max of 4 digits
674 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0xFFFF), std.mem.nativeToLittle(u16, 'f') }, try parseQuotedWideString(arena, .{
675 .slice =
676 \\L"\XfFfFf"
677 ,
678 .code_page = .windows1252,
679 }, .{}));
680 // octal max of 7 digits
681 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0x9493), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '3') }, try parseQuotedWideString(arena, .{
682 .slice =
683 \\L"\111222333"
684 ,
685 .code_page = .windows1252,
686 }, .{}));
687 // octal overflow
688 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{std.mem.nativeToLittle(u16, 0xFF01)}, try parseQuotedWideString(arena, .{
689 .slice =
690 \\L"\777401"
691 ,
692 .code_page = .windows1252,
693 }, .{}));
694 // literal tab characters get converted to spaces (dependent on source file columns)
695 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("abcdefg "), try parseQuotedWideString(
696 arena,
697 .{ .slice = "L\"abcdefg\t\"", .code_page = .windows1252 },
698 .{},
699 ));
700 // Windows-1252 conversion
701 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("ðð€€€"), try parseQuotedWideString(
702 arena,
703 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .windows1252 },
704 .{},
705 ));
706 // Invalid escape sequences are skipped
707 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedWideString(
708 arena,
709 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
710 .{},
711 ));
712}
713
714test "parse quoted wide string with utf8 code page" {
715 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
716 defer arena_allocator.deinit();
717 const arena = arena_allocator.allocator();
718
719 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{}, try parseQuotedWideString(
720 arena,
721 .{ .slice = "L\"\"", .code_page = .utf8 },
722 .{},
723 ));
724 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedWideString(
725 arena,
726 .{ .slice = "L\"кириллица\"", .code_page = .utf8 },
727 .{},
728 ));
729 // Invalid UTF-8 gets converted to � depending on well-formedness
730 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("����"), try parseQuotedWideString(
731 arena,
732 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
733 .{},
734 ));
735}
736
737test "parse quoted ascii string as wide string" {
738 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
739 defer arena_allocator.deinit();
740 const arena = arena_allocator.allocator();
741
742 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedStringAsWideString(
743 arena,
744 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
745 .{},
746 ));
747 // Whether or not invalid escapes are skipped is still determined by the L prefix
748 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("\\H"), try parseQuotedStringAsWideString(
749 arena,
750 .{ .slice = "\"\\H\"", .code_page = .windows1252 },
751 .{},
752 ));
753 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedStringAsWideString(
754 arena,
755 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
756 .{},
757 ));
758 // Maximum escape sequence value is also determined by the L prefix
759 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0x12), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '4') }, try parseQuotedStringAsWideString(
760 arena,
761 .{ .slice = "\"\\x1234\"", .code_page = .windows1252 },
762 .{},
763 ));
764 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{std.mem.nativeToLittle(u16, 0x1234)}, try parseQuotedStringAsWideString(
765 arena,
766 .{ .slice = "L\"\\x1234\"", .code_page = .windows1252 },
767 .{},
768 ));
769}
770
771pub fn columnsUntilTabStop(column: usize, tab_columns: usize) usize {
772 // 0 => 8, 1 => 7, 2 => 6, 3 => 5, 4 => 4
773 // 5 => 3, 6 => 2, 7 => 1, 8 => 8
774 return tab_columns - (column % tab_columns);
775}
776
777pub fn columnWidth(cur_column: usize, c: u8, tab_columns: usize) usize {
778 return switch (c) {
779 '\t' => columnsUntilTabStop(cur_column, tab_columns),
780 else => 1,
781 };
782}
783
784pub const Number = struct {
785 value: u32,
786 is_long: bool = false,
787
788 pub fn asWord(self: Number) u16 {
789 return @truncate(self.value);
790 }
791
792 pub fn evaluateOperator(lhs: Number, operator_char: u8, rhs: Number) Number {
793 const result = switch (operator_char) {
794 '-' => lhs.value -% rhs.value,
795 '+' => lhs.value +% rhs.value,
796 '|' => lhs.value | rhs.value,
797 '&' => lhs.value & rhs.value,
798 else => unreachable, // invalid operator, this would be a lexer/parser bug
799 };
800 return .{
801 .value = result,
802 .is_long = lhs.is_long or rhs.is_long,
803 };
804 }
805};
806
807/// Assumes that number literals normally rejected by RC's preprocessor
808/// are similarly rejected before being parsed.
809///
810/// Relevant RC preprocessor errors:
811/// RC2021: expected exponent value, not '<digit>'
812/// example that is rejected: 1e1
813/// example that is accepted: 1ea
814/// (this function will parse the two examples above the same)
815pub fn parseNumberLiteral(bytes: SourceBytes) Number {
816 std.debug.assert(bytes.slice.len > 0);
817 var result = Number{ .value = 0, .is_long = false };
818 var radix: u8 = 10;
819 var buf = bytes.slice;
820
821 const Prefix = enum { none, minus, complement };
822 var prefix: Prefix = .none;
823 switch (buf[0]) {
824 '-' => {
825 prefix = .minus;
826 buf = buf[1..];
827 },
828 '~' => {
829 prefix = .complement;
830 buf = buf[1..];
831 },
832 else => {},
833 }
834
835 if (buf.len > 2 and buf[0] == '0') {
836 switch (buf[1]) {
837 'o' => { // octal radix prefix is case-sensitive
838 radix = 8;
839 buf = buf[2..];
840 },
841 'x', 'X' => {
842 radix = 16;
843 buf = buf[2..];
844 },
845 else => {},
846 }
847 }
848
849 var i: usize = 0;
850 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
851 const c = codepoint.value;
852 if (c == 'L' or c == 'l') {
853 result.is_long = true;
854 break;
855 }
856 const digit = switch (c) {
857 // On invalid digit for the radix, just stop parsing but don't fail
858 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch break,
859 else => break,
860 };
861
862 if (result.value != 0) {
863 result.value *%= radix;
864 }
865 result.value +%= digit;
866 }
867
868 switch (prefix) {
869 .none => {},
870 .minus => result.value = 0 -% result.value,
871 .complement => result.value = ~result.value,
872 }
873
874 return result;
875}
876
877test "parse number literal" {
878 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "0", .code_page = .windows1252 }));
879 try std.testing.expectEqual(Number{ .value = 1, .is_long = false }, parseNumberLiteral(.{ .slice = "1", .code_page = .windows1252 }));
880 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "1L", .code_page = .windows1252 }));
881 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "1l", .code_page = .windows1252 }));
882 try std.testing.expectEqual(Number{ .value = 1, .is_long = false }, parseNumberLiteral(.{ .slice = "1garbageL", .code_page = .windows1252 }));
883 try std.testing.expectEqual(Number{ .value = 4294967295, .is_long = false }, parseNumberLiteral(.{ .slice = "4294967295", .code_page = .windows1252 }));
884 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "4294967296", .code_page = .windows1252 }));
885 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "4294967297L", .code_page = .windows1252 }));
886
887 // can handle any length of number, wraps on overflow appropriately
888 const big_overflow = parseNumberLiteral(.{ .slice = "1000000000000000000000000000000000000000000000000000000000000000000000000000000090000000001", .code_page = .windows1252 });
889 try std.testing.expectEqual(Number{ .value = 4100654081, .is_long = false }, big_overflow);
890 try std.testing.expectEqual(@as(u16, 1025), big_overflow.asWord());
891
892 try std.testing.expectEqual(Number{ .value = 0x20, .is_long = false }, parseNumberLiteral(.{ .slice = "0x20", .code_page = .windows1252 }));
893 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2AL", .code_page = .windows1252 }));
894 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL", .code_page = .windows1252 }));
895 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL", .code_page = .windows1252 }));
896
897 try std.testing.expectEqual(Number{ .value = 0o20, .is_long = false }, parseNumberLiteral(.{ .slice = "0o20", .code_page = .windows1252 }));
898 try std.testing.expectEqual(Number{ .value = 0o20, .is_long = true }, parseNumberLiteral(.{ .slice = "0o20L", .code_page = .windows1252 }));
899 try std.testing.expectEqual(Number{ .value = 0o2, .is_long = false }, parseNumberLiteral(.{ .slice = "0o29", .code_page = .windows1252 }));
900 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "0O29", .code_page = .windows1252 }));
901
902 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFF, .is_long = false }, parseNumberLiteral(.{ .slice = "-1", .code_page = .windows1252 }));
903 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFE, .is_long = false }, parseNumberLiteral(.{ .slice = "~1", .code_page = .windows1252 }));
904 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFF, .is_long = true }, parseNumberLiteral(.{ .slice = "-4294967297L", .code_page = .windows1252 }));
905 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFE, .is_long = true }, parseNumberLiteral(.{ .slice = "~4294967297L", .code_page = .windows1252 }));
906 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFD, .is_long = false }, parseNumberLiteral(.{ .slice = "-0X3", .code_page = .windows1252 }));
907
908 // anything after L is ignored
909 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL5", .code_page = .windows1252 }));
910}
lib/compiler/resinator/main.zig created+298
...@@ -0,0 +1,298 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const removeComments = @import("comments.zig").removeComments;
4const parseAndRemoveLineCommands = @import("source_mapping.zig").parseAndRemoveLineCommands;
5const compile = @import("compile.zig").compile;
6const Diagnostics = @import("errors.zig").Diagnostics;
7const cli = @import("cli.zig");
8const preprocess = @import("preprocess.zig");
9const renderErrorMessage = @import("utils.zig").renderErrorMessage;
10const aro = @import("aro");
11
12pub fn main() !void {
13 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
14 defer std.debug.assert(gpa.deinit() == .ok);
15 const allocator = gpa.allocator();
16
17 const stderr = std.io.getStdErr();
18 const stderr_config = std.io.tty.detectConfig(stderr);
19
20 const args = try std.process.argsAlloc(allocator);
21 defer std.process.argsFree(allocator, args);
22
23 if (args.len < 2) {
24 try renderErrorMessage(stderr.writer(), stderr_config, .err, "expected zig lib dir as first argument", .{});
25 std.os.exit(1);
26 }
27 const zig_lib_dir = args[1];
28
29 var options = options: {
30 var cli_diagnostics = cli.Diagnostics.init(allocator);
31 defer cli_diagnostics.deinit();
32 var options = cli.parse(allocator, args[2..], &cli_diagnostics) catch |err| switch (err) {
33 error.ParseError => {
34 cli_diagnostics.renderToStdErr(args, stderr_config);
35 std.os.exit(1);
36 },
37 else => |e| return e,
38 };
39 try options.maybeAppendRC(std.fs.cwd());
40
41 // print any warnings/notes
42 cli_diagnostics.renderToStdErr(args, stderr_config);
43 // If there was something printed, then add an extra newline separator
44 // so that there is a clear separation between the cli diagnostics and whatever
45 // gets printed after
46 if (cli_diagnostics.errors.items.len > 0) {
47 try stderr.writeAll("\n");
48 }
49 break :options options;
50 };
51 defer options.deinit();
52
53 if (options.print_help_and_exit) {
54 try cli.writeUsage(stderr.writer(), "zig rc");
55 return;
56 }
57
58 const stdout_writer = std.io.getStdOut().writer();
59 if (options.verbose) {
60 try options.dumpVerbose(stdout_writer);
61 try stdout_writer.writeByte('\n');
62 }
63
64 var dependencies_list = std.ArrayList([]const u8).init(allocator);
65 defer {
66 for (dependencies_list.items) |item| {
67 allocator.free(item);
68 }
69 dependencies_list.deinit();
70 }
71 const maybe_dependencies_list: ?*std.ArrayList([]const u8) = if (options.depfile_path != null) &dependencies_list else null;
72
73 const full_input = full_input: {
74 if (options.preprocess != .no) {
75 var preprocessed_buf = std.ArrayList(u8).init(allocator);
76 errdefer preprocessed_buf.deinit();
77
78 // We're going to throw away everything except the final preprocessed output anyway,
79 // so we can use a scoped arena for everything else.
80 var aro_arena_state = std.heap.ArenaAllocator.init(allocator);
81 defer aro_arena_state.deinit();
82 const aro_arena = aro_arena_state.allocator();
83
84 const include_paths = getIncludePaths(aro_arena, options.auto_includes, zig_lib_dir) catch |err| switch (err) {
85 error.OutOfMemory => |e| return e,
86 else => |e| {
87 switch (e) {
88 error.MsvcIncludesNotFound => {
89 try renderErrorMessage(stderr.writer(), stderr_config, .err, "MSVC include paths could not be automatically detected", .{});
90 },
91 error.MingwIncludesNotFound => {
92 try renderErrorMessage(stderr.writer(), stderr_config, .err, "MinGW include paths could not be automatically detected", .{});
93 },
94 }
95 try renderErrorMessage(stderr.writer(), stderr_config, .note, "to disable auto includes, use the option /:auto-includes none", .{});
96 std.os.exit(1);
97 },
98 };
99
100 var comp = aro.Compilation.init(aro_arena);
101 defer comp.deinit();
102
103 var argv = std.ArrayList([]const u8).init(comp.gpa);
104 defer argv.deinit();
105
106 try argv.append("arocc"); // dummy command name
107 try preprocess.appendAroArgs(aro_arena, &argv, options, include_paths);
108 try argv.append(options.input_filename);
109
110 if (options.verbose) {
111 try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n");
112 for (argv.items[0 .. argv.items.len - 1]) |arg| {
113 try stdout_writer.print("{s} ", .{arg});
114 }
115 try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]});
116 }
117
118 preprocess.preprocess(&comp, preprocessed_buf.writer(), argv.items, maybe_dependencies_list) catch |err| switch (err) {
119 error.GeneratedSourceError => {
120 // extra newline to separate this line from the aro errors
121 try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessor setup (this is always a bug):\n", .{});
122 aro.Diagnostics.render(&comp, stderr_config);
123 std.os.exit(1);
124 },
125 // ArgError can occur if e.g. the .rc file is not found
126 error.ArgError, error.PreprocessError => {
127 // extra newline to separate this line from the aro errors
128 try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing:\n", .{});
129 aro.Diagnostics.render(&comp, stderr_config);
130 std.os.exit(1);
131 },
132 error.StreamTooLong => {
133 try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing: maximum file size exceeded", .{});
134 std.os.exit(1);
135 },
136 error.OutOfMemory => |e| return e,
137 };
138
139 break :full_input try preprocessed_buf.toOwnedSlice();
140 } else {
141 break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| {
142 try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
143 std.os.exit(1);
144 };
145 }
146 };
147 defer allocator.free(full_input);
148
149 if (options.preprocess == .only) {
150 try std.fs.cwd().writeFile(options.output_filename, full_input);
151 return;
152 }
153
154 // Note: We still want to run this when no-preprocess is set because:
155 // 1. We want to print accurate line numbers after removing multiline comments
156 // 2. We want to be able to handle an already-preprocessed input with #line commands in it
157 var mapping_results = try parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename });
158 defer mapping_results.mappings.deinit(allocator);
159
160 const final_input = removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings) catch |err| switch (err) {
161 error.InvalidSourceMappingCollapse => {
162 try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during comment removal; this is a known bug", .{});
163 std.os.exit(1);
164 },
165 else => |e| return e,
166 };
167
168 var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
169 try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
170 std.os.exit(1);
171 };
172 var output_file_closed = false;
173 defer if (!output_file_closed) output_file.close();
174
175 var diagnostics = Diagnostics.init(allocator);
176 defer diagnostics.deinit();
177
178 var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
179
180 compile(allocator, final_input, output_buffered_stream.writer(), .{
181 .cwd = std.fs.cwd(),
182 .diagnostics = &diagnostics,
183 .source_mappings = &mapping_results.mappings,
184 .dependencies_list = maybe_dependencies_list,
185 .ignore_include_env_var = options.ignore_include_env_var,
186 .extra_include_paths = options.extra_include_paths.items,
187 .default_language_id = options.default_language_id,
188 .default_code_page = options.default_code_page orelse .windows1252,
189 .verbose = options.verbose,
190 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
191 .max_string_literal_codepoints = options.max_string_literal_codepoints,
192 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
193 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
194 }) catch |err| switch (err) {
195 error.ParseError, error.CompileError => {
196 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
197 // Delete the output file on error
198 output_file.close();
199 output_file_closed = true;
200 // Failing to delete is not really a big deal, so swallow any errors
201 std.fs.cwd().deleteFile(options.output_filename) catch {};
202 std.os.exit(1);
203 },
204 else => |e| return e,
205 };
206
207 try output_buffered_stream.flush();
208
209 // print any warnings/notes
210 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
211
212 // write the depfile
213 if (options.depfile_path) |depfile_path| {
214 var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| {
215 try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) });
216 std.os.exit(1);
217 };
218 defer depfile.close();
219
220 const depfile_writer = depfile.writer();
221 var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer);
222 switch (options.depfile_fmt) {
223 .json => {
224 var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 });
225 defer write_stream.deinit();
226
227 try write_stream.beginArray();
228 for (dependencies_list.items) |dep_path| {
229 try write_stream.write(dep_path);
230 }
231 try write_stream.endArray();
232 },
233 }
234 try depfile_buffered_writer.flush();
235 }
236}
237
238fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 {
239 var includes = auto_includes_option;
240 if (builtin.target.os.tag != .windows) {
241 switch (includes) {
242 // MSVC can't be found when the host isn't Windows, so short-circuit.
243 .msvc => return error.MsvcIncludesNotFound,
244 // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts.
245 .any => includes = .gnu,
246 .none, .gnu => {},
247 }
248 }
249
250 while (true) {
251 switch (includes) {
252 .none => return &[_][]const u8{},
253 .any, .msvc => {
254 // MSVC is only detectable on Windows targets. This unreachable is to signify
255 // that .any and .msvc should be dealt with on non-Windows targets before this point,
256 // since getting MSVC include paths uses Windows-only APIs.
257 if (builtin.target.os.tag != .windows) unreachable;
258
259 const target_query: std.Target.Query = .{
260 .os_tag = .windows,
261 .abi = .msvc,
262 };
263 const target = std.zig.resolveTargetQueryOrFatal(target_query);
264 const is_native_abi = target_query.isNativeAbi();
265 const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch {
266 if (includes == .any) {
267 // fall back to mingw
268 includes = .gnu;
269 continue;
270 }
271 return error.MsvcIncludesNotFound;
272 };
273 if (detected_libc.libc_include_dir_list.len == 0) {
274 if (includes == .any) {
275 // fall back to mingw
276 includes = .gnu;
277 continue;
278 }
279 return error.MsvcIncludesNotFound;
280 }
281 return detected_libc.libc_include_dir_list;
282 },
283 .gnu => {
284 const target_query: std.Target.Query = .{
285 .os_tag = .windows,
286 .abi = .gnu,
287 };
288 const target = std.zig.resolveTargetQueryOrFatal(target_query);
289 const is_native_abi = target_query.isNativeAbi();
290 const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| switch (err) {
291 error.OutOfMemory => |e| return e,
292 else => return error.MingwIncludesNotFound,
293 };
294 return detected_libc.libc_include_dir_list;
295 },
296 }
297 }
298}
lib/compiler/resinator/parse.zig created+1897
...@@ -0,0 +1,1897 @@
1const std = @import("std");
2const Lexer = @import("lex.zig").Lexer;
3const Token = @import("lex.zig").Token;
4const Node = @import("ast.zig").Node;
5const Tree = @import("ast.zig").Tree;
6const CodePageLookup = @import("ast.zig").CodePageLookup;
7const Resource = @import("rc.zig").Resource;
8const Allocator = std.mem.Allocator;
9const ErrorDetails = @import("errors.zig").ErrorDetails;
10const Diagnostics = @import("errors.zig").Diagnostics;
11const SourceBytes = @import("literals.zig").SourceBytes;
12const Compiler = @import("compile.zig").Compiler;
13const rc = @import("rc.zig");
14const res = @import("res.zig");
15
16// TODO: Make these configurable?
17pub const max_nested_menu_level: u32 = 512;
18pub const max_nested_version_level: u32 = 512;
19pub const max_nested_expression_level: u32 = 200;
20
21pub const Parser = struct {
22 const Self = @This();
23
24 lexer: *Lexer,
25 /// values that need to be initialized per-parse
26 state: Parser.State = undefined,
27 options: Parser.Options,
28
29 pub const Error = error{ParseError} || Allocator.Error;
30
31 pub const Options = struct {
32 warn_instead_of_error_on_invalid_code_page: bool = false,
33 };
34
35 pub fn init(lexer: *Lexer, options: Options) Parser {
36 return Parser{
37 .lexer = lexer,
38 .options = options,
39 };
40 }
41
42 pub const State = struct {
43 token: Token,
44 lookahead_lexer: Lexer,
45 allocator: Allocator,
46 arena: Allocator,
47 diagnostics: *Diagnostics,
48 input_code_page_lookup: CodePageLookup,
49 output_code_page_lookup: CodePageLookup,
50 };
51
52 pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree {
53 var arena = std.heap.ArenaAllocator.init(allocator);
54 errdefer arena.deinit();
55
56 self.state = Parser.State{
57 .token = undefined,
58 .lookahead_lexer = undefined,
59 .allocator = allocator,
60 .arena = arena.allocator(),
61 .diagnostics = diagnostics,
62 .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
63 .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
64 };
65
66 const parsed_root = try self.parseRoot();
67
68 const tree = try self.state.arena.create(Tree);
69 tree.* = .{
70 .node = parsed_root,
71 .input_code_pages = self.state.input_code_page_lookup,
72 .output_code_pages = self.state.output_code_page_lookup,
73 .source = self.lexer.buffer,
74 .arena = arena.state,
75 .allocator = allocator,
76 };
77 return tree;
78 }
79
80 fn parseRoot(self: *Self) Error!*Node {
81 var statements = std.ArrayList(*Node).init(self.state.allocator);
82 defer statements.deinit();
83
84 try self.parseStatements(&statements);
85 try self.check(.eof);
86
87 const node = try self.state.arena.create(Node.Root);
88 node.* = .{
89 .body = try self.state.arena.dupe(*Node, statements.items),
90 };
91 return &node.base;
92 }
93
94 fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void {
95 while (true) {
96 try self.nextToken(.whitespace_delimiter_only);
97 if (self.state.token.id == .eof) break;
98 // The Win32 compiler will sometimes try to recover from errors
99 // and then restart parsing afterwards. We don't ever do this
100 // because it almost always leads to unhelpful error messages
101 // (usually it will end up with bogus things like 'file
102 // not found: {')
103 const 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 const value_node = try self.state.arena.create(Node.Literal);
178 value_node.* = .{
179 .token = value,
180 };
181 const node = try self.state.arena.create(Node.SimpleStatement);
182 node.* = .{
183 .identifier = identifier,
184 .value = &value_node.base,
185 };
186 try optional_statements.append(self.state.arena, &node.base);
187 },
188 // String or number
189 .class => {
190 const identifier = self.state.token;
191 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
192 const node = try self.state.arena.create(Node.SimpleStatement);
193 node.* = .{
194 .identifier = identifier,
195 .value = value,
196 };
197 try optional_statements.append(self.state.arena, &node.base);
198 },
199 // Special case
200 .menu => {
201 const identifier = self.state.token;
202 try self.nextToken(.whitespace_delimiter_only);
203 try self.check(.literal);
204 const value_node = try self.state.arena.create(Node.Literal);
205 value_node.* = .{
206 .token = self.state.token,
207 };
208 const node = try self.state.arena.create(Node.SimpleStatement);
209 node.* = .{
210 .identifier = identifier,
211 .value = &value_node.base,
212 };
213 try optional_statements.append(self.state.arena, &node.base);
214 },
215 .font => {
216 const identifier = self.state.token;
217 const point_size = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
218
219 // The comma between point_size and typeface is both optional and
220 // there can be any number of them
221 try self.skipAnyCommas();
222
223 try self.nextToken(.normal);
224 const typeface = self.state.token;
225 if (!typeface.isStringLiteral()) {
226 return self.addErrorDetailsAndFail(ErrorDetails{
227 .err = .expected_something_else,
228 .token = typeface,
229 .extra = .{ .expected_types = .{
230 .string_literal = true,
231 } },
232 });
233 }
234
235 const ExSpecificValues = struct {
236 weight: ?*Node = null,
237 italic: ?*Node = null,
238 char_set: ?*Node = null,
239 };
240 var ex_specific = ExSpecificValues{};
241 ex_specific: {
242 var optional_param_parser = OptionalParamParser{ .parser = self };
243 switch (resource) {
244 .dialogex => {
245 {
246 ex_specific.weight = try optional_param_parser.parse(.{});
247 if (optional_param_parser.finished) break :ex_specific;
248 }
249 {
250 if (!(try self.parseOptionalToken(.comma))) break :ex_specific;
251 ex_specific.italic = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
252 }
253 {
254 ex_specific.char_set = try optional_param_parser.parse(.{});
255 if (optional_param_parser.finished) break :ex_specific;
256 }
257 },
258 .dialog => {},
259 else => unreachable, // only DIALOG and DIALOGEX have FONT optional-statements
260 }
261 }
262
263 const node = try self.state.arena.create(Node.FontStatement);
264 node.* = .{
265 .identifier = identifier,
266 .point_size = point_size,
267 .typeface = typeface,
268 .weight = ex_specific.weight,
269 .italic = ex_specific.italic,
270 .char_set = ex_specific.char_set,
271 };
272 try optional_statements.append(self.state.arena, &node.base);
273 },
274 }
275 }
276 return optional_statements.toOwnedSlice(self.state.arena);
277 }
278
279 /// Expects the current token to be the first token of the statement.
280 fn parseStatement(self: *Self) Error!*Node {
281 const first_token = self.state.token;
282 std.debug.assert(first_token.id == .literal);
283
284 if (rc.TopLevelKeywords.map.get(first_token.slice(self.lexer.buffer))) |keyword| switch (keyword) {
285 .language => {
286 const language_statement = try self.parseLanguageStatement();
287 return language_statement;
288 },
289 .version, .characteristics => {
290 const identifier = self.state.token;
291 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
292 const node = try self.state.arena.create(Node.SimpleStatement);
293 node.* = .{
294 .identifier = identifier,
295 .value = value,
296 };
297 return &node.base;
298 },
299 .stringtable => {
300 // common resource attributes must all be contiguous and come before optional-statements
301 const common_resource_attributes = try self.parseCommonResourceAttributes();
302 const optional_statements = try self.parseOptionalStatements(.stringtable);
303
304 try self.nextToken(.normal);
305 const begin_token = self.state.token;
306 try self.check(.begin);
307
308 var strings = std.ArrayList(*Node).init(self.state.allocator);
309 defer strings.deinit();
310 while (true) {
311 const maybe_end_token = try self.lookaheadToken(.normal);
312 switch (maybe_end_token.id) {
313 .end => {
314 self.nextToken(.normal) catch unreachable;
315 break;
316 },
317 .eof => {
318 return self.addErrorDetailsAndFail(ErrorDetails{
319 .err = .unfinished_string_table_block,
320 .token = maybe_end_token,
321 });
322 },
323 else => {},
324 }
325 const id_expression = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
326
327 const comma_token: ?Token = if (try self.parseOptionalToken(.comma)) self.state.token else null;
328
329 try self.nextToken(.normal);
330 if (self.state.token.id != .quoted_ascii_string and self.state.token.id != .quoted_wide_string) {
331 return self.addErrorDetailsAndFail(ErrorDetails{
332 .err = .expected_something_else,
333 .token = self.state.token,
334 .extra = .{ .expected_types = .{ .string_literal = true } },
335 });
336 }
337
338 const string_node = try self.state.arena.create(Node.StringTableString);
339 string_node.* = .{
340 .id = id_expression,
341 .maybe_comma = comma_token,
342 .string = self.state.token,
343 };
344 try strings.append(&string_node.base);
345 }
346
347 if (strings.items.len == 0) {
348 return self.addErrorDetailsAndFail(ErrorDetails{
349 .err = .expected_token, // TODO: probably a more specific error message
350 .token = self.state.token,
351 .extra = .{ .expected = .number },
352 });
353 }
354
355 const end_token = self.state.token;
356 try self.check(.end);
357
358 const node = try self.state.arena.create(Node.StringTable);
359 node.* = .{
360 .type = first_token,
361 .common_resource_attributes = common_resource_attributes,
362 .optional_statements = optional_statements,
363 .begin_token = begin_token,
364 .strings = try self.state.arena.dupe(*Node, strings.items),
365 .end_token = end_token,
366 };
367 return &node.base;
368 },
369 };
370
371 // The Win32 RC compiler allows for a 'dangling' literal at the end of a file
372 // (as long as it's not a valid top-level keyword), and there is actually an
373 // .rc file with a such a dangling literal in the Windows-classic-samples set
374 // of projects. So, we have special compatibility for this particular case.
375 const maybe_eof = try self.lookaheadToken(.whitespace_delimiter_only);
376 if (maybe_eof.id == .eof) {
377 // TODO: emit warning
378 var context = try self.state.arena.alloc(Token, 2);
379 context[0] = first_token;
380 context[1] = maybe_eof;
381 const invalid_node = try self.state.arena.create(Node.Invalid);
382 invalid_node.* = .{
383 .context = context,
384 };
385 return &invalid_node.base;
386 }
387
388 const id_token = first_token;
389 const id_code_page = self.lexer.current_code_page;
390 try self.nextToken(.whitespace_delimiter_only);
391 const resource = try self.checkResource();
392 const type_token = self.state.token;
393
394 if (resource == .string_num) {
395 try self.addErrorDetails(.{
396 .err = .string_resource_as_numeric_type,
397 .token = type_token,
398 });
399 return self.addErrorDetailsAndFail(.{
400 .err = .string_resource_as_numeric_type,
401 .token = type_token,
402 .type = .note,
403 .print_source_line = false,
404 });
405 }
406
407 if (resource == .font) {
408 const id_bytes = SourceBytes{
409 .slice = id_token.slice(self.lexer.buffer),
410 .code_page = id_code_page,
411 };
412 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(id_bytes);
413 if (maybe_ordinal == null) {
414 const would_be_win32_rc_ordinal = res.NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes);
415 if (would_be_win32_rc_ordinal) |win32_rc_ordinal| {
416 try self.addErrorDetails(ErrorDetails{
417 .err = .id_must_be_ordinal,
418 .token = id_token,
419 .extra = .{ .resource = resource },
420 });
421 return self.addErrorDetailsAndFail(ErrorDetails{
422 .err = .win32_non_ascii_ordinal,
423 .token = id_token,
424 .type = .note,
425 .print_source_line = false,
426 .extra = .{ .number = win32_rc_ordinal.ordinal },
427 });
428 } else {
429 return self.addErrorDetailsAndFail(ErrorDetails{
430 .err = .id_must_be_ordinal,
431 .token = id_token,
432 .extra = .{ .resource = resource },
433 });
434 }
435 }
436 }
437
438 switch (resource) {
439 .accelerators => {
440 // common resource attributes must all be contiguous and come before optional-statements
441 const common_resource_attributes = try self.parseCommonResourceAttributes();
442 const optional_statements = try self.parseOptionalStatements(resource);
443
444 try self.nextToken(.normal);
445 const begin_token = self.state.token;
446 try self.check(.begin);
447
448 var accelerators = std.ArrayListUnmanaged(*Node){};
449
450 while (true) {
451 const lookahead = try self.lookaheadToken(.normal);
452 switch (lookahead.id) {
453 .end, .eof => {
454 self.nextToken(.normal) catch unreachable;
455 break;
456 },
457 else => {},
458 }
459 const event = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
460
461 try self.nextToken(.normal);
462 try self.check(.comma);
463
464 const idvalue = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
465
466 var type_and_options = std.ArrayListUnmanaged(Token){};
467 while (true) {
468 if (!(try self.parseOptionalToken(.comma))) break;
469
470 try self.nextToken(.normal);
471 if (!rc.AcceleratorTypeAndOptions.map.has(self.tokenSlice())) {
472 return self.addErrorDetailsAndFail(.{
473 .err = .expected_something_else,
474 .token = self.state.token,
475 .extra = .{ .expected_types = .{
476 .accelerator_type_or_option = true,
477 } },
478 });
479 }
480 try type_and_options.append(self.state.arena, self.state.token);
481 }
482
483 const node = try self.state.arena.create(Node.Accelerator);
484 node.* = .{
485 .event = event,
486 .idvalue = idvalue,
487 .type_and_options = try type_and_options.toOwnedSlice(self.state.arena),
488 };
489 try accelerators.append(self.state.arena, &node.base);
490 }
491
492 const end_token = self.state.token;
493 try self.check(.end);
494
495 const node = try self.state.arena.create(Node.Accelerators);
496 node.* = .{
497 .id = id_token,
498 .type = type_token,
499 .common_resource_attributes = common_resource_attributes,
500 .optional_statements = optional_statements,
501 .begin_token = begin_token,
502 .accelerators = try accelerators.toOwnedSlice(self.state.arena),
503 .end_token = end_token,
504 };
505 return &node.base;
506 },
507 .dialog, .dialogex => {
508 // common resource attributes must all be contiguous and come before optional-statements
509 const common_resource_attributes = try self.parseCommonResourceAttributes();
510
511 const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
512 _ = try self.parseOptionalToken(.comma);
513
514 const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
515 _ = try self.parseOptionalToken(.comma);
516
517 const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
518 _ = try self.parseOptionalToken(.comma);
519
520 const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
521
522 var optional_param_parser = OptionalParamParser{ .parser = self };
523 const help_id: ?*Node = try optional_param_parser.parse(.{});
524
525 const optional_statements = try self.parseOptionalStatements(resource);
526
527 try self.nextToken(.normal);
528 const begin_token = self.state.token;
529 try self.check(.begin);
530
531 var controls = std.ArrayListUnmanaged(*Node){};
532 defer controls.deinit(self.state.allocator);
533 while (try self.parseControlStatement(resource)) |control_node| {
534 // The number of controls must fit in a u16 in order for it to
535 // be able to be written into the relevant field in the .res data.
536 if (controls.items.len >= std.math.maxInt(u16)) {
537 try self.addErrorDetails(.{
538 .err = .too_many_dialog_controls_or_toolbar_buttons,
539 .token = id_token,
540 .extra = .{ .resource = resource },
541 });
542 return self.addErrorDetailsAndFail(.{
543 .err = .too_many_dialog_controls_or_toolbar_buttons,
544 .type = .note,
545 .token = control_node.getFirstToken(),
546 .token_span_end = control_node.getLastToken(),
547 .extra = .{ .resource = resource },
548 });
549 }
550
551 try controls.append(self.state.allocator, control_node);
552 }
553
554 try self.nextToken(.normal);
555 const end_token = self.state.token;
556 try self.check(.end);
557
558 const node = try self.state.arena.create(Node.Dialog);
559 node.* = .{
560 .id = id_token,
561 .type = type_token,
562 .common_resource_attributes = common_resource_attributes,
563 .x = x,
564 .y = y,
565 .width = width,
566 .height = height,
567 .help_id = help_id,
568 .optional_statements = optional_statements,
569 .begin_token = begin_token,
570 .controls = try self.state.arena.dupe(*Node, controls.items),
571 .end_token = end_token,
572 };
573 return &node.base;
574 },
575 .toolbar => {
576 // common resource attributes must all be contiguous and come before optional-statements
577 const common_resource_attributes = try self.parseCommonResourceAttributes();
578
579 const button_width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
580
581 try self.nextToken(.normal);
582 try self.check(.comma);
583
584 const button_height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
585
586 try self.nextToken(.normal);
587 const begin_token = self.state.token;
588 try self.check(.begin);
589
590 var buttons = std.ArrayListUnmanaged(*Node){};
591 defer buttons.deinit(self.state.allocator);
592 while (try self.parseToolbarButtonStatement()) |button_node| {
593 // The number of buttons must fit in a u16 in order for it to
594 // be able to be written into the relevant field in the .res data.
595 if (buttons.items.len >= std.math.maxInt(u16)) {
596 try self.addErrorDetails(.{
597 .err = .too_many_dialog_controls_or_toolbar_buttons,
598 .token = id_token,
599 .extra = .{ .resource = resource },
600 });
601 return self.addErrorDetailsAndFail(.{
602 .err = .too_many_dialog_controls_or_toolbar_buttons,
603 .type = .note,
604 .token = button_node.getFirstToken(),
605 .token_span_end = button_node.getLastToken(),
606 .extra = .{ .resource = resource },
607 });
608 }
609
610 try buttons.append(self.state.allocator, button_node);
611 }
612
613 try self.nextToken(.normal);
614 const end_token = self.state.token;
615 try self.check(.end);
616
617 const node = try self.state.arena.create(Node.Toolbar);
618 node.* = .{
619 .id = id_token,
620 .type = type_token,
621 .common_resource_attributes = common_resource_attributes,
622 .button_width = button_width,
623 .button_height = button_height,
624 .begin_token = begin_token,
625 .buttons = try self.state.arena.dupe(*Node, buttons.items),
626 .end_token = end_token,
627 };
628 return &node.base;
629 },
630 .menu, .menuex => {
631 // common resource attributes must all be contiguous and come before optional-statements
632 const common_resource_attributes = try self.parseCommonResourceAttributes();
633 // help id is optional but must come between common resource attributes and optional-statements
634 var help_id: ?*Node = null;
635 // Note: No comma is allowed before or after help_id of MENUEX and help_id is not
636 // a possible field of MENU.
637 if (resource == .menuex and try self.lookaheadCouldBeNumberExpression(.not_disallowed)) {
638 help_id = try self.parseExpression(.{
639 .is_known_to_be_number_expression = true,
640 });
641 }
642 const optional_statements = try self.parseOptionalStatements(.stringtable);
643
644 try self.nextToken(.normal);
645 const begin_token = self.state.token;
646 try self.check(.begin);
647
648 var items = std.ArrayListUnmanaged(*Node){};
649 defer items.deinit(self.state.allocator);
650 while (try self.parseMenuItemStatement(resource, id_token, 1)) |item_node| {
651 try items.append(self.state.allocator, item_node);
652 }
653
654 try self.nextToken(.normal);
655 const end_token = self.state.token;
656 try self.check(.end);
657
658 if (items.items.len == 0) {
659 return self.addErrorDetailsAndFail(.{
660 .err = .empty_menu_not_allowed,
661 .token = type_token,
662 });
663 }
664
665 const node = try self.state.arena.create(Node.Menu);
666 node.* = .{
667 .id = id_token,
668 .type = type_token,
669 .common_resource_attributes = common_resource_attributes,
670 .optional_statements = optional_statements,
671 .help_id = help_id,
672 .begin_token = begin_token,
673 .items = try self.state.arena.dupe(*Node, items.items),
674 .end_token = end_token,
675 };
676 return &node.base;
677 },
678 .versioninfo => {
679 // common resource attributes must all be contiguous and come before optional-statements
680 const common_resource_attributes = try self.parseCommonResourceAttributes();
681
682 var fixed_info = std.ArrayListUnmanaged(*Node){};
683 while (try self.parseVersionStatement()) |version_statement| {
684 try fixed_info.append(self.state.arena, version_statement);
685 }
686
687 try self.nextToken(.normal);
688 const begin_token = self.state.token;
689 try self.check(.begin);
690
691 var block_statements = std.ArrayListUnmanaged(*Node){};
692 while (try self.parseVersionBlockOrValue(id_token, 1)) |block_node| {
693 try block_statements.append(self.state.arena, block_node);
694 }
695
696 try self.nextToken(.normal);
697 const end_token = self.state.token;
698 try self.check(.end);
699
700 const node = try self.state.arena.create(Node.VersionInfo);
701 node.* = .{
702 .id = id_token,
703 .versioninfo = type_token,
704 .common_resource_attributes = common_resource_attributes,
705 .fixed_info = try fixed_info.toOwnedSlice(self.state.arena),
706 .begin_token = begin_token,
707 .block_statements = try block_statements.toOwnedSlice(self.state.arena),
708 .end_token = end_token,
709 };
710 return &node.base;
711 },
712 .dlginclude => {
713 const common_resource_attributes = try self.parseCommonResourceAttributes();
714
715 const filename_expression = try self.parseExpression(.{
716 .allowed_types = .{ .string = true },
717 });
718
719 const node = try self.state.arena.create(Node.ResourceExternal);
720 node.* = .{
721 .id = id_token,
722 .type = type_token,
723 .common_resource_attributes = common_resource_attributes,
724 .filename = filename_expression,
725 };
726 return &node.base;
727 },
728 .stringtable => {
729 return self.addErrorDetailsAndFail(.{
730 .err = .name_or_id_not_allowed,
731 .token = id_token,
732 .extra = .{ .resource = resource },
733 });
734 },
735 // Just try everything as a 'generic' resource (raw data or external file)
736 // TODO: More fine-grained switch cases as necessary
737 else => {
738 const common_resource_attributes = try self.parseCommonResourceAttributes();
739
740 const maybe_begin = try self.lookaheadToken(.normal);
741 if (maybe_begin.id == .begin) {
742 self.nextToken(.normal) catch unreachable;
743
744 if (!resource.canUseRawData()) {
745 try self.addErrorDetails(ErrorDetails{
746 .err = .resource_type_cant_use_raw_data,
747 .token = maybe_begin,
748 .extra = .{ .resource = resource },
749 });
750 return self.addErrorDetailsAndFail(ErrorDetails{
751 .err = .resource_type_cant_use_raw_data,
752 .type = .note,
753 .print_source_line = false,
754 .token = maybe_begin,
755 });
756 }
757
758 const raw_data = try self.parseRawDataBlock();
759 const end_token = self.state.token;
760
761 const node = try self.state.arena.create(Node.ResourceRawData);
762 node.* = .{
763 .id = id_token,
764 .type = type_token,
765 .common_resource_attributes = common_resource_attributes,
766 .begin_token = maybe_begin,
767 .raw_data = raw_data,
768 .end_token = end_token,
769 };
770 return &node.base;
771 }
772
773 const filename_expression = try self.parseExpression(.{
774 // Don't tell the user that numbers are accepted since we error on
775 // number expressions and regular number literals are treated as unquoted
776 // literals rather than numbers, so from the users perspective
777 // numbers aren't really allowed.
778 .expected_types_override = .{
779 .literal = true,
780 .string_literal = true,
781 },
782 });
783
784 const node = try self.state.arena.create(Node.ResourceExternal);
785 node.* = .{
786 .id = id_token,
787 .type = type_token,
788 .common_resource_attributes = common_resource_attributes,
789 .filename = filename_expression,
790 };
791 return &node.base;
792 },
793 }
794 }
795
796 /// Expects the current token to be a begin token.
797 /// After return, the current token will be the end token.
798 fn parseRawDataBlock(self: *Self) Error![]*Node {
799 var raw_data = std.ArrayList(*Node).init(self.state.allocator);
800 defer raw_data.deinit();
801 while (true) {
802 const maybe_end_token = try self.lookaheadToken(.normal);
803 switch (maybe_end_token.id) {
804 .comma => {
805 // comma as the first token in a raw data block is an error
806 if (raw_data.items.len == 0) {
807 return self.addErrorDetailsAndFail(ErrorDetails{
808 .err = .expected_something_else,
809 .token = maybe_end_token,
810 .extra = .{ .expected_types = .{
811 .number = true,
812 .number_expression = true,
813 .string_literal = true,
814 } },
815 });
816 }
817 // otherwise just skip over commas
818 self.nextToken(.normal) catch unreachable;
819 continue;
820 },
821 .end => {
822 self.nextToken(.normal) catch unreachable;
823 break;
824 },
825 .eof => {
826 return self.addErrorDetailsAndFail(ErrorDetails{
827 .err = .unfinished_raw_data_block,
828 .token = maybe_end_token,
829 });
830 },
831 else => {},
832 }
833 const expression = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
834 try raw_data.append(expression);
835
836 if (expression.isNumberExpression()) {
837 const maybe_close_paren = try self.lookaheadToken(.normal);
838 if (maybe_close_paren.id == .close_paren) {
839 // <number expression>) is an error
840 return self.addErrorDetailsAndFail(ErrorDetails{
841 .err = .expected_token,
842 .token = maybe_close_paren,
843 .extra = .{ .expected = .operator },
844 });
845 }
846 }
847 }
848 return try self.state.arena.dupe(*Node, raw_data.items);
849 }
850
851 /// Expects the current token to be handled, and that the control statement will
852 /// begin on the next token.
853 /// After return, the current token will be the token immediately before the end of the
854 /// control statement (or unchanged if the function returns null).
855 fn parseControlStatement(self: *Self, resource: Resource) Error!?*Node {
856 const control_token = try self.lookaheadToken(.normal);
857 const control = rc.Control.map.get(control_token.slice(self.lexer.buffer)) orelse return null;
858 self.nextToken(.normal) catch unreachable;
859
860 try self.skipAnyCommas();
861
862 var text: ?Token = null;
863 if (control.hasTextParam()) {
864 try self.nextToken(.normal);
865 switch (self.state.token.id) {
866 .quoted_ascii_string, .quoted_wide_string, .number => {
867 text = self.state.token;
868 },
869 else => {
870 return self.addErrorDetailsAndFail(ErrorDetails{
871 .err = .expected_something_else,
872 .token = self.state.token,
873 .extra = .{ .expected_types = .{
874 .number = true,
875 .string_literal = true,
876 } },
877 });
878 },
879 }
880 try self.skipAnyCommas();
881 }
882
883 const id = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
884
885 try self.skipAnyCommas();
886
887 var class: ?*Node = null;
888 var style: ?*Node = null;
889 if (control == .control) {
890 class = try self.parseExpression(.{});
891 if (class.?.id == .literal) {
892 const class_literal = @fieldParentPtr(Node.Literal, "base", class.?);
893 const is_invalid_control_class = class_literal.token.id == .literal and !rc.ControlClass.map.has(class_literal.token.slice(self.lexer.buffer));
894 if (is_invalid_control_class) {
895 return self.addErrorDetailsAndFail(.{
896 .err = .expected_something_else,
897 .token = self.state.token,
898 .extra = .{ .expected_types = .{
899 .control_class = true,
900 } },
901 });
902 }
903 }
904 try self.skipAnyCommas();
905 style = try self.parseExpression(.{
906 .can_contain_not_expressions = true,
907 .allowed_types = .{ .number = true },
908 });
909 // If there is no comma after the style paramter, the Win32 RC compiler
910 // could misinterpret the statement and end up skipping over at least one token
911 // that should have been interepeted as the next parameter (x). For example:
912 // CONTROL "text", 1, BUTTON, 15 30, 1, 2, 3, 4
913 // the `15` is the style parameter, but in the Win32 implementation the `30`
914 // is completely ignored (i.e. the `1, 2, 3, 4` are `x`, `y`, `w`, `h`).
915 // If a comma is added after the `15`, then `30` gets interpreted (correctly)
916 // as the `x` value.
917 //
918 // Instead of emulating this behavior, we just warn about the potential for
919 // weird behavior in the Win32 implementation whenever there isn't a comma after
920 // the style parameter.
921 const lookahead_token = try self.lookaheadToken(.normal);
922 if (lookahead_token.id != .comma and lookahead_token.id != .eof) {
923 try self.addErrorDetails(.{
924 .err = .rc_could_miscompile_control_params,
925 .type = .warning,
926 .token = lookahead_token,
927 });
928 try self.addErrorDetails(.{
929 .err = .rc_could_miscompile_control_params,
930 .type = .note,
931 .token = style.?.getFirstToken(),
932 .token_span_end = style.?.getLastToken(),
933 });
934 }
935 try self.skipAnyCommas();
936 }
937
938 const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
939 _ = try self.parseOptionalToken(.comma);
940 const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
941 _ = try self.parseOptionalToken(.comma);
942 const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
943 _ = try self.parseOptionalToken(.comma);
944 const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
945
946 var optional_param_parser = OptionalParamParser{ .parser = self };
947 if (control != .control) {
948 style = try optional_param_parser.parse(.{ .not_expression_allowed = true });
949 }
950
951 const exstyle: ?*Node = try optional_param_parser.parse(.{ .not_expression_allowed = true });
952 const help_id: ?*Node = switch (resource) {
953 .dialogex => try optional_param_parser.parse(.{}),
954 else => null,
955 };
956
957 var extra_data: []*Node = &[_]*Node{};
958 var extra_data_begin: ?Token = null;
959 var extra_data_end: ?Token = null;
960 // extra data is DIALOGEX-only
961 if (resource == .dialogex and try self.parseOptionalToken(.begin)) {
962 extra_data_begin = self.state.token;
963 extra_data = try self.parseRawDataBlock();
964 extra_data_end = self.state.token;
965 }
966
967 const node = try self.state.arena.create(Node.ControlStatement);
968 node.* = .{
969 .type = control_token,
970 .text = text,
971 .class = class,
972 .id = id,
973 .x = x,
974 .y = y,
975 .width = width,
976 .height = height,
977 .style = style,
978 .exstyle = exstyle,
979 .help_id = help_id,
980 .extra_data_begin = extra_data_begin,
981 .extra_data = extra_data,
982 .extra_data_end = extra_data_end,
983 };
984 return &node.base;
985 }
986
987 fn parseToolbarButtonStatement(self: *Self) Error!?*Node {
988 const keyword_token = try self.lookaheadToken(.normal);
989 const button_type = rc.ToolbarButton.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
990 self.nextToken(.normal) catch unreachable;
991
992 switch (button_type) {
993 .separator => {
994 const node = try self.state.arena.create(Node.Literal);
995 node.* = .{
996 .token = keyword_token,
997 };
998 return &node.base;
999 },
1000 .button => {
1001 const button_id = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1002
1003 const node = try self.state.arena.create(Node.SimpleStatement);
1004 node.* = .{
1005 .identifier = keyword_token,
1006 .value = button_id,
1007 };
1008 return &node.base;
1009 },
1010 }
1011 }
1012
1013 /// Expects the current token to be handled, and that the menuitem/popup statement will
1014 /// begin on the next token.
1015 /// After return, the current token will be the token immediately before the end of the
1016 /// menuitem statement (or unchanged if the function returns null).
1017 fn parseMenuItemStatement(self: *Self, resource: Resource, top_level_menu_id_token: Token, nesting_level: u32) Error!?*Node {
1018 const menuitem_token = try self.lookaheadToken(.normal);
1019 const menuitem = rc.MenuItem.map.get(menuitem_token.slice(self.lexer.buffer)) orelse return null;
1020 self.nextToken(.normal) catch unreachable;
1021
1022 if (nesting_level > max_nested_menu_level) {
1023 try self.addErrorDetails(.{
1024 .err = .nested_resource_level_exceeds_max,
1025 .token = top_level_menu_id_token,
1026 .extra = .{ .resource = resource },
1027 });
1028 return self.addErrorDetailsAndFail(.{
1029 .err = .nested_resource_level_exceeds_max,
1030 .type = .note,
1031 .token = menuitem_token,
1032 .extra = .{ .resource = resource },
1033 });
1034 }
1035
1036 switch (resource) {
1037 .menu => switch (menuitem) {
1038 .menuitem => {
1039 try self.nextToken(.normal);
1040 if (rc.MenuItem.isSeparator(self.state.token.slice(self.lexer.buffer))) {
1041 const separator_token = self.state.token;
1042 // There can be any number of trailing commas after SEPARATOR
1043 try self.skipAnyCommas();
1044 const node = try self.state.arena.create(Node.MenuItemSeparator);
1045 node.* = .{
1046 .menuitem = menuitem_token,
1047 .separator = separator_token,
1048 };
1049 return &node.base;
1050 } else {
1051 const text = self.state.token;
1052 if (!text.isStringLiteral()) {
1053 return self.addErrorDetailsAndFail(ErrorDetails{
1054 .err = .expected_something_else,
1055 .token = text,
1056 .extra = .{ .expected_types = .{
1057 .string_literal = true,
1058 } },
1059 });
1060 }
1061 try self.skipAnyCommas();
1062
1063 const result = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1064
1065 _ = try self.parseOptionalToken(.comma);
1066
1067 var options = std.ArrayListUnmanaged(Token){};
1068 while (true) {
1069 const option_token = try self.lookaheadToken(.normal);
1070 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
1071 break;
1072 }
1073 self.nextToken(.normal) catch unreachable;
1074 try options.append(self.state.arena, option_token);
1075 try self.skipAnyCommas();
1076 }
1077
1078 const node = try self.state.arena.create(Node.MenuItem);
1079 node.* = .{
1080 .menuitem = menuitem_token,
1081 .text = text,
1082 .result = result,
1083 .option_list = try options.toOwnedSlice(self.state.arena),
1084 };
1085 return &node.base;
1086 }
1087 },
1088 .popup => {
1089 try self.nextToken(.normal);
1090 const text = self.state.token;
1091 if (!text.isStringLiteral()) {
1092 return self.addErrorDetailsAndFail(ErrorDetails{
1093 .err = .expected_something_else,
1094 .token = text,
1095 .extra = .{ .expected_types = .{
1096 .string_literal = true,
1097 } },
1098 });
1099 }
1100 try self.skipAnyCommas();
1101
1102 var options = std.ArrayListUnmanaged(Token){};
1103 while (true) {
1104 const option_token = try self.lookaheadToken(.normal);
1105 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
1106 break;
1107 }
1108 self.nextToken(.normal) catch unreachable;
1109 try options.append(self.state.arena, option_token);
1110 try self.skipAnyCommas();
1111 }
1112
1113 try self.nextToken(.normal);
1114 const begin_token = self.state.token;
1115 try self.check(.begin);
1116
1117 var items = std.ArrayListUnmanaged(*Node){};
1118 while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| {
1119 try items.append(self.state.arena, item_node);
1120 }
1121
1122 try self.nextToken(.normal);
1123 const end_token = self.state.token;
1124 try self.check(.end);
1125
1126 if (items.items.len == 0) {
1127 return self.addErrorDetailsAndFail(.{
1128 .err = .empty_menu_not_allowed,
1129 .token = menuitem_token,
1130 });
1131 }
1132
1133 const node = try self.state.arena.create(Node.Popup);
1134 node.* = .{
1135 .popup = menuitem_token,
1136 .text = text,
1137 .option_list = try options.toOwnedSlice(self.state.arena),
1138 .begin_token = begin_token,
1139 .items = try items.toOwnedSlice(self.state.arena),
1140 .end_token = end_token,
1141 };
1142 return &node.base;
1143 },
1144 },
1145 .menuex => {
1146 try self.nextToken(.normal);
1147 const text = self.state.token;
1148 if (!text.isStringLiteral()) {
1149 return self.addErrorDetailsAndFail(ErrorDetails{
1150 .err = .expected_something_else,
1151 .token = text,
1152 .extra = .{ .expected_types = .{
1153 .string_literal = true,
1154 } },
1155 });
1156 }
1157
1158 var param_parser = OptionalParamParser{ .parser = self };
1159 const id = try param_parser.parse(.{});
1160 const item_type = try param_parser.parse(.{});
1161 const state = try param_parser.parse(.{});
1162
1163 if (menuitem == .menuitem) {
1164 // trailing comma is allowed, skip it
1165 _ = try self.parseOptionalToken(.comma);
1166
1167 const node = try self.state.arena.create(Node.MenuItemEx);
1168 node.* = .{
1169 .menuitem = menuitem_token,
1170 .text = text,
1171 .id = id,
1172 .type = item_type,
1173 .state = state,
1174 };
1175 return &node.base;
1176 }
1177
1178 const help_id = try param_parser.parse(.{});
1179
1180 // trailing comma is allowed, skip it
1181 _ = try self.parseOptionalToken(.comma);
1182
1183 try self.nextToken(.normal);
1184 const begin_token = self.state.token;
1185 try self.check(.begin);
1186
1187 var items = std.ArrayListUnmanaged(*Node){};
1188 while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| {
1189 try items.append(self.state.arena, item_node);
1190 }
1191
1192 try self.nextToken(.normal);
1193 const end_token = self.state.token;
1194 try self.check(.end);
1195
1196 if (items.items.len == 0) {
1197 return self.addErrorDetailsAndFail(.{
1198 .err = .empty_menu_not_allowed,
1199 .token = menuitem_token,
1200 });
1201 }
1202
1203 const node = try self.state.arena.create(Node.PopupEx);
1204 node.* = .{
1205 .popup = menuitem_token,
1206 .text = text,
1207 .id = id,
1208 .type = item_type,
1209 .state = state,
1210 .help_id = help_id,
1211 .begin_token = begin_token,
1212 .items = try items.toOwnedSlice(self.state.arena),
1213 .end_token = end_token,
1214 };
1215 return &node.base;
1216 },
1217 else => unreachable,
1218 }
1219 @compileError("unreachable");
1220 }
1221
1222 pub const OptionalParamParser = struct {
1223 finished: bool = false,
1224 parser: *Self,
1225
1226 pub const Options = struct {
1227 not_expression_allowed: bool = false,
1228 };
1229
1230 pub fn parse(self: *OptionalParamParser, options: OptionalParamParser.Options) Error!?*Node {
1231 if (self.finished) return null;
1232 if (!(try self.parser.parseOptionalToken(.comma))) {
1233 self.finished = true;
1234 return null;
1235 }
1236 // If the next lookahead token could be part of a number expression,
1237 // then parse it. Otherwise, treat it as an 'empty' expression and
1238 // continue parsing, since 'empty' values are allowed.
1239 if (try self.parser.lookaheadCouldBeNumberExpression(switch (options.not_expression_allowed) {
1240 true => .not_allowed,
1241 false => .not_disallowed,
1242 })) {
1243 const node = try self.parser.parseExpression(.{
1244 .allowed_types = .{ .number = true },
1245 .can_contain_not_expressions = options.not_expression_allowed,
1246 });
1247 return node;
1248 }
1249 return null;
1250 }
1251 };
1252
1253 /// Expects the current token to be handled, and that the version statement will
1254 /// begin on the next token.
1255 /// After return, the current token will be the token immediately before the end of the
1256 /// version statement (or unchanged if the function returns null).
1257 fn parseVersionStatement(self: *Self) Error!?*Node {
1258 const type_token = try self.lookaheadToken(.normal);
1259 const statement_type = rc.VersionInfo.map.get(type_token.slice(self.lexer.buffer)) orelse return null;
1260 self.nextToken(.normal) catch unreachable;
1261 switch (statement_type) {
1262 .file_version, .product_version => {
1263 var parts_buffer: [4]*Node = undefined;
1264 var parts = std.ArrayListUnmanaged(*Node).initBuffer(&parts_buffer);
1265
1266 while (true) {
1267 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1268 parts.addOneAssumeCapacity().* = value;
1269
1270 if (parts.unusedCapacitySlice().len == 0 or
1271 !(try self.parseOptionalToken(.comma)))
1272 {
1273 break;
1274 }
1275 }
1276
1277 const node = try self.state.arena.create(Node.VersionStatement);
1278 node.* = .{
1279 .type = type_token,
1280 .parts = try self.state.arena.dupe(*Node, parts.items),
1281 };
1282 return &node.base;
1283 },
1284 else => {
1285 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1286
1287 const node = try self.state.arena.create(Node.SimpleStatement);
1288 node.* = .{
1289 .identifier = type_token,
1290 .value = value,
1291 };
1292 return &node.base;
1293 },
1294 }
1295 }
1296
1297 /// Expects the current token to be handled, and that the version BLOCK/VALUE will
1298 /// begin on the next token.
1299 /// After return, the current token will be the token immediately before the end of the
1300 /// version BLOCK/VALUE (or unchanged if the function returns null).
1301 fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node {
1302 const keyword_token = try self.lookaheadToken(.normal);
1303 const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
1304 self.nextToken(.normal) catch unreachable;
1305
1306 if (nesting_level > max_nested_version_level) {
1307 try self.addErrorDetails(.{
1308 .err = .nested_resource_level_exceeds_max,
1309 .token = top_level_version_id_token,
1310 .extra = .{ .resource = .versioninfo },
1311 });
1312 return self.addErrorDetailsAndFail(.{
1313 .err = .nested_resource_level_exceeds_max,
1314 .type = .note,
1315 .token = keyword_token,
1316 .extra = .{ .resource = .versioninfo },
1317 });
1318 }
1319
1320 try self.nextToken(.normal);
1321 const key = self.state.token;
1322 if (!key.isStringLiteral()) {
1323 return self.addErrorDetailsAndFail(.{
1324 .err = .expected_something_else,
1325 .token = key,
1326 .extra = .{ .expected_types = .{
1327 .string_literal = true,
1328 } },
1329 });
1330 }
1331 // Need to keep track of this to detect a potential miscompilation when
1332 // the comma is omitted and the first value is a quoted string.
1333 const had_comma_before_first_value = try self.parseOptionalToken(.comma);
1334 try self.skipAnyCommas();
1335
1336 const values = try self.parseBlockValuesList(had_comma_before_first_value);
1337
1338 switch (keyword) {
1339 .block => {
1340 try self.nextToken(.normal);
1341 const begin_token = self.state.token;
1342 try self.check(.begin);
1343
1344 var children = std.ArrayListUnmanaged(*Node){};
1345 while (try self.parseVersionBlockOrValue(top_level_version_id_token, nesting_level + 1)) |value_node| {
1346 try children.append(self.state.arena, value_node);
1347 }
1348
1349 try self.nextToken(.normal);
1350 const end_token = self.state.token;
1351 try self.check(.end);
1352
1353 const node = try self.state.arena.create(Node.Block);
1354 node.* = .{
1355 .identifier = keyword_token,
1356 .key = key,
1357 .values = values,
1358 .begin_token = begin_token,
1359 .children = try children.toOwnedSlice(self.state.arena),
1360 .end_token = end_token,
1361 };
1362 return &node.base;
1363 },
1364 .value => {
1365 const node = try self.state.arena.create(Node.BlockValue);
1366 node.* = .{
1367 .identifier = keyword_token,
1368 .key = key,
1369 .values = values,
1370 };
1371 return &node.base;
1372 },
1373 }
1374 }
1375
1376 fn parseBlockValuesList(self: *Self, had_comma_before_first_value: bool) Error![]*Node {
1377 var values = std.ArrayListUnmanaged(*Node){};
1378 var seen_number: bool = false;
1379 var first_string_value: ?*Node = null;
1380 while (true) {
1381 const lookahead_token = try self.lookaheadToken(.normal);
1382 switch (lookahead_token.id) {
1383 .operator,
1384 .number,
1385 .open_paren,
1386 .quoted_ascii_string,
1387 .quoted_wide_string,
1388 => {},
1389 else => break,
1390 }
1391 const value = try self.parseExpression(.{});
1392
1393 if (value.isNumberExpression()) {
1394 seen_number = true;
1395 } else if (first_string_value == null) {
1396 std.debug.assert(value.isStringLiteral());
1397 first_string_value = value;
1398 }
1399
1400 const has_trailing_comma = try self.parseOptionalToken(.comma);
1401 try self.skipAnyCommas();
1402
1403 const value_value = try self.state.arena.create(Node.BlockValueValue);
1404 value_value.* = .{
1405 .expression = value,
1406 .trailing_comma = has_trailing_comma,
1407 };
1408 try values.append(self.state.arena, &value_value.base);
1409 }
1410 if (seen_number and first_string_value != null) {
1411 // The Win32 RC compiler does some strange stuff with the data size:
1412 // Strings are counted as UTF-16 code units including the null-terminator
1413 // Numbers are counted as their byte lengths
1414 // So, when both strings and numbers are within a single value,
1415 // it incorrectly sets the value's type as binary, but then gives the
1416 // data length as a mixture of bytes and UTF-16 code units. This means that
1417 // when the length is read, it will be treated as byte length and will
1418 // not read the full value. We don't reproduce this behavior, so we warn
1419 // of the miscompilation here.
1420 try self.addErrorDetails(.{
1421 .err = .rc_would_miscompile_version_value_byte_count,
1422 .type = .warning,
1423 .token = first_string_value.?.getFirstToken(),
1424 .token_span_start = values.items[0].getFirstToken(),
1425 .token_span_end = values.items[values.items.len - 1].getLastToken(),
1426 });
1427 try self.addErrorDetails(.{
1428 .err = .rc_would_miscompile_version_value_byte_count,
1429 .type = .note,
1430 .token = first_string_value.?.getFirstToken(),
1431 .token_span_start = values.items[0].getFirstToken(),
1432 .token_span_end = values.items[values.items.len - 1].getLastToken(),
1433 .print_source_line = false,
1434 });
1435 }
1436 if (!had_comma_before_first_value and values.items.len > 0 and values.items[0].cast(.block_value_value).?.expression.isStringLiteral()) {
1437 const token = values.items[0].cast(.block_value_value).?.expression.cast(.literal).?.token;
1438 try self.addErrorDetails(.{
1439 .err = .rc_would_miscompile_version_value_padding,
1440 .type = .warning,
1441 .token = token,
1442 });
1443 try self.addErrorDetails(.{
1444 .err = .rc_would_miscompile_version_value_padding,
1445 .type = .note,
1446 .token = token,
1447 .print_source_line = false,
1448 });
1449 }
1450 return values.toOwnedSlice(self.state.arena);
1451 }
1452
1453 fn numberExpressionContainsAnyLSuffixes(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) bool {
1454 // TODO: This could probably be done without evaluating the whole expression
1455 return Compiler.evaluateNumberExpression(expression_node, source, code_page_lookup).is_long;
1456 }
1457
1458 /// Expects the current token to be a literal token that contains the string LANGUAGE
1459 fn parseLanguageStatement(self: *Self) Error!*Node {
1460 const language_token = self.state.token;
1461
1462 const primary_language = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1463
1464 try self.nextToken(.normal);
1465 try self.check(.comma);
1466
1467 const sublanguage = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1468
1469 // The Win32 RC compiler errors if either parameter contains any number with an L
1470 // suffix. Instead of that, we want to warn and then let the values get truncated.
1471 // The warning is done here to allow the compiler logic to not have to deal with this.
1472 if (numberExpressionContainsAnyLSuffixes(primary_language, 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 = primary_language.getFirstToken(),
1477 .token_span_end = primary_language.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 = primary_language.getFirstToken(),
1485 .token_span_end = primary_language.getLastToken(),
1486 .extra = .{ .statement_with_u16_param = .language },
1487 });
1488 }
1489 if (numberExpressionContainsAnyLSuffixes(sublanguage, self.lexer.buffer, &self.state.input_code_page_lookup)) {
1490 try self.addErrorDetails(.{
1491 .err = .rc_would_error_u16_with_l_suffix,
1492 .type = .warning,
1493 .token = sublanguage.getFirstToken(),
1494 .token_span_end = sublanguage.getLastToken(),
1495 .extra = .{ .statement_with_u16_param = .language },
1496 });
1497 try self.addErrorDetails(.{
1498 .err = .rc_would_error_u16_with_l_suffix,
1499 .print_source_line = false,
1500 .type = .note,
1501 .token = sublanguage.getFirstToken(),
1502 .token_span_end = sublanguage.getLastToken(),
1503 .extra = .{ .statement_with_u16_param = .language },
1504 });
1505 }
1506
1507 const node = try self.state.arena.create(Node.LanguageStatement);
1508 node.* = .{
1509 .language_token = language_token,
1510 .primary_language_id = primary_language,
1511 .sublanguage_id = sublanguage,
1512 };
1513 return &node.base;
1514 }
1515
1516 pub const ParseExpressionOptions = struct {
1517 is_known_to_be_number_expression: bool = false,
1518 can_contain_not_expressions: bool = false,
1519 nesting_context: NestingContext = .{},
1520 allowed_types: AllowedTypes = .{ .literal = true, .number = true, .string = true },
1521 expected_types_override: ?ErrorDetails.ExpectedTypes = null,
1522
1523 pub const AllowedTypes = struct {
1524 literal: bool = false,
1525 number: bool = false,
1526 string: bool = false,
1527 };
1528
1529 pub const NestingContext = struct {
1530 first_token: ?Token = null,
1531 last_token: ?Token = null,
1532 level: u32 = 0,
1533
1534 /// Returns a new NestingContext with values modified appropriately for an increased nesting level
1535 fn incremented(ctx: NestingContext, first_token: Token, most_recent_token: Token) NestingContext {
1536 return .{
1537 .first_token = ctx.first_token orelse first_token,
1538 .last_token = most_recent_token,
1539 .level = ctx.level + 1,
1540 };
1541 }
1542 };
1543
1544 pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetails {
1545 // TODO: expected_types_override interaction with is_known_to_be_number_expression?
1546 const expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{
1547 .number = options.allowed_types.number,
1548 .number_expression = options.allowed_types.number,
1549 .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression,
1550 .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression,
1551 };
1552 return ErrorDetails{
1553 .err = .expected_something_else,
1554 .token = token,
1555 .extra = .{ .expected_types = expected_types },
1556 };
1557 }
1558 };
1559
1560 /// Returns true if the next lookahead token is a number or could be the start of a number expression.
1561 /// Only useful when looking for empty expressions in optional fields.
1562 fn lookaheadCouldBeNumberExpression(self: *Self, not_allowed: enum { not_allowed, not_disallowed }) Error!bool {
1563 var lookahead_token = try self.lookaheadToken(.normal);
1564 switch (lookahead_token.id) {
1565 .literal => if (not_allowed == .not_allowed) {
1566 return std.ascii.eqlIgnoreCase("NOT", lookahead_token.slice(self.lexer.buffer));
1567 } else return false,
1568 .number => return true,
1569 .open_paren => return true,
1570 .operator => {
1571 // + can be a unary operator, see parseExpression's handling of unary +
1572 const operator_char = lookahead_token.slice(self.lexer.buffer)[0];
1573 return operator_char == '+';
1574 },
1575 else => return false,
1576 }
1577 }
1578
1579 fn parsePrimary(self: *Self, options: ParseExpressionOptions) Error!*Node {
1580 try self.nextToken(.normal);
1581 const first_token = self.state.token;
1582 var is_close_paren_expression = false;
1583 var is_unary_plus_expression = false;
1584 switch (self.state.token.id) {
1585 .quoted_ascii_string, .quoted_wide_string => {
1586 if (!options.allowed_types.string) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1587 const node = try self.state.arena.create(Node.Literal);
1588 node.* = .{ .token = self.state.token };
1589 return &node.base;
1590 },
1591 .literal => {
1592 if (options.can_contain_not_expressions and std.ascii.eqlIgnoreCase("NOT", self.state.token.slice(self.lexer.buffer))) {
1593 const not_token = self.state.token;
1594 try self.nextToken(.normal);
1595 try self.check(.number);
1596 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1597 const node = try self.state.arena.create(Node.NotExpression);
1598 node.* = .{
1599 .not_token = not_token,
1600 .number_token = self.state.token,
1601 };
1602 return &node.base;
1603 }
1604 if (!options.allowed_types.literal) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1605 const node = try self.state.arena.create(Node.Literal);
1606 node.* = .{ .token = self.state.token };
1607 return &node.base;
1608 },
1609 .number => {
1610 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1611 const node = try self.state.arena.create(Node.Literal);
1612 node.* = .{ .token = self.state.token };
1613 return &node.base;
1614 },
1615 .open_paren => {
1616 const open_paren_token = self.state.token;
1617
1618 const expression = try self.parseExpression(.{
1619 .is_known_to_be_number_expression = true,
1620 .can_contain_not_expressions = options.can_contain_not_expressions,
1621 .nesting_context = options.nesting_context.incremented(first_token, open_paren_token),
1622 .allowed_types = .{ .number = true },
1623 });
1624
1625 try self.nextToken(.normal);
1626 // TODO: Add context to error about where the open paren is
1627 try self.check(.close_paren);
1628
1629 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(open_paren_token));
1630 const node = try self.state.arena.create(Node.GroupedExpression);
1631 node.* = .{
1632 .open_token = open_paren_token,
1633 .expression = expression,
1634 .close_token = self.state.token,
1635 };
1636 return &node.base;
1637 },
1638 .close_paren => {
1639 // Note: In the Win32 implementation, a single close paren
1640 // counts as a valid "expression", but only when its the first and
1641 // only token in the expression. Such an expression is then treated
1642 // as a 'skip this expression' instruction. For example:
1643 // 1 RCDATA { 1, ), ), ), 2 }
1644 // will be evaluated as if it were `1 RCDATA { 1, 2 }` and only
1645 // 0x0001 and 0x0002 will be written to the .res data.
1646 //
1647 // This behavior is not emulated because it almost certainly has
1648 // no valid use cases and only introduces edge cases that are
1649 // not worth the effort to track down and deal with. Instead,
1650 // we error but also add a note about the Win32 RC behavior if
1651 // this edge case is detected.
1652 if (!options.is_known_to_be_number_expression) {
1653 is_close_paren_expression = true;
1654 }
1655 },
1656 .operator => {
1657 // In the Win32 implementation, something akin to a unary +
1658 // is allowed but it doesn't behave exactly like a unary +.
1659 // Instead of emulating the Win32 behavior, we instead error
1660 // and add a note about unary plus not being allowed.
1661 //
1662 // This is done because unary + only works in some places,
1663 // and there's no real use-case for it since it's so limited
1664 // in how it can be used (e.g. +1 is accepted but (+1) will error)
1665 //
1666 // Even understanding when unary plus is allowed is difficult, so
1667 // we don't do any fancy detection of when the Win32 RC compiler would
1668 // allow a unary + and instead just output the note in all cases.
1669 //
1670 // Some examples of allowed expressions by the Win32 compiler:
1671 // +1
1672 // 0|+5
1673 // +1+2
1674 // +~-5
1675 // +(1)
1676 //
1677 // Some examples of disallowed expressions by the Win32 compiler:
1678 // (+1)
1679 // ++5
1680 //
1681 // TODO: Potentially re-evaluate and support the unary plus in a bug-for-bug
1682 // compatible way.
1683 const operator_char = self.state.token.slice(self.lexer.buffer)[0];
1684 if (operator_char == '+') {
1685 is_unary_plus_expression = true;
1686 }
1687 },
1688 else => {},
1689 }
1690
1691 try self.addErrorDetails(options.toErrorDetails(self.state.token));
1692 if (is_close_paren_expression) {
1693 try self.addErrorDetails(ErrorDetails{
1694 .err = .close_paren_expression,
1695 .type = .note,
1696 .token = self.state.token,
1697 .print_source_line = false,
1698 });
1699 }
1700 if (is_unary_plus_expression) {
1701 try self.addErrorDetails(ErrorDetails{
1702 .err = .unary_plus_expression,
1703 .type = .note,
1704 .token = self.state.token,
1705 .print_source_line = false,
1706 });
1707 }
1708 return error.ParseError;
1709 }
1710
1711 /// Expects the current token to have already been dealt with, and that the
1712 /// expression will start on the next token.
1713 /// After return, the current token will have been dealt with.
1714 fn parseExpression(self: *Self, options: ParseExpressionOptions) Error!*Node {
1715 if (options.nesting_context.level > max_nested_expression_level) {
1716 try self.addErrorDetails(.{
1717 .err = .nested_expression_level_exceeds_max,
1718 .token = options.nesting_context.first_token.?,
1719 });
1720 return self.addErrorDetailsAndFail(.{
1721 .err = .nested_expression_level_exceeds_max,
1722 .type = .note,
1723 .token = options.nesting_context.last_token.?,
1724 });
1725 }
1726 var expr: *Node = try self.parsePrimary(options);
1727 const first_token = expr.getFirstToken();
1728
1729 // Non-number expressions can't have operators, so we can just return
1730 if (!expr.isNumberExpression()) return expr;
1731
1732 while (try self.parseOptionalTokenAdvanced(.operator, .normal_expect_operator)) {
1733 const operator = self.state.token;
1734 const rhs_node = try self.parsePrimary(.{
1735 .is_known_to_be_number_expression = true,
1736 .can_contain_not_expressions = options.can_contain_not_expressions,
1737 .nesting_context = options.nesting_context.incremented(first_token, operator),
1738 .allowed_types = options.allowed_types,
1739 });
1740
1741 if (!rhs_node.isNumberExpression()) {
1742 return self.addErrorDetailsAndFail(ErrorDetails{
1743 .err = .expected_something_else,
1744 .token = rhs_node.getFirstToken(),
1745 .token_span_end = rhs_node.getLastToken(),
1746 .extra = .{ .expected_types = .{
1747 .number = true,
1748 .number_expression = true,
1749 } },
1750 });
1751 }
1752
1753 const node = try self.state.arena.create(Node.BinaryExpression);
1754 node.* = .{
1755 .left = expr,
1756 .operator = operator,
1757 .right = rhs_node,
1758 };
1759 expr = &node.base;
1760 }
1761
1762 return expr;
1763 }
1764
1765 /// Skips any amount of commas (including zero)
1766 /// In other words, it will skip the regex `,*`
1767 /// Assumes the token(s) should be parsed with `.normal` as the method.
1768 fn skipAnyCommas(self: *Self) !void {
1769 while (try self.parseOptionalToken(.comma)) {}
1770 }
1771
1772 /// Advances the current token only if the token's id matches the specified `id`.
1773 /// Assumes the token should be parsed with `.normal` as the method.
1774 /// Returns true if the token matched, false otherwise.
1775 fn parseOptionalToken(self: *Self, id: Token.Id) Error!bool {
1776 return self.parseOptionalTokenAdvanced(id, .normal);
1777 }
1778
1779 /// Advances the current token only if the token's id matches the specified `id`.
1780 /// Returns true if the token matched, false otherwise.
1781 fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool {
1782 const maybe_token = try self.lookaheadToken(method);
1783 if (maybe_token.id != id) return false;
1784 self.nextToken(method) catch unreachable;
1785 return true;
1786 }
1787
1788 fn addErrorDetails(self: *Self, details: ErrorDetails) Allocator.Error!void {
1789 try self.state.diagnostics.append(details);
1790 }
1791
1792 fn addErrorDetailsAndFail(self: *Self, details: ErrorDetails) Error {
1793 try self.addErrorDetails(details);
1794 return error.ParseError;
1795 }
1796
1797 fn nextToken(self: *Self, comptime method: Lexer.LexMethod) Error!void {
1798 self.state.token = token: while (true) {
1799 const token = self.lexer.next(method) catch |err| switch (err) {
1800 error.CodePagePragmaInIncludedFile => {
1801 // The Win32 RC compiler silently ignores such `#pragma code_point` directives,
1802 // but we want to both ignore them *and* emit a warning
1803 try self.addErrorDetails(.{
1804 .err = .code_page_pragma_in_included_file,
1805 .type = .warning,
1806 .token = self.lexer.error_context_token.?,
1807 });
1808 continue;
1809 },
1810 error.CodePagePragmaInvalidCodePage => {
1811 var details = self.lexer.getErrorDetails(err);
1812 if (!self.options.warn_instead_of_error_on_invalid_code_page) {
1813 return self.addErrorDetailsAndFail(details);
1814 }
1815 details.type = .warning;
1816 try self.addErrorDetails(details);
1817 continue;
1818 },
1819 error.InvalidDigitCharacterInNumberLiteral => {
1820 const details = self.lexer.getErrorDetails(err);
1821 try self.addErrorDetails(details);
1822 return self.addErrorDetailsAndFail(.{
1823 .err = details.err,
1824 .type = .note,
1825 .token = details.token,
1826 .print_source_line = false,
1827 });
1828 },
1829 else => return self.addErrorDetailsAndFail(self.lexer.getErrorDetails(err)),
1830 };
1831 break :token token;
1832 };
1833 // After every token, set the input code page for its line
1834 try self.state.input_code_page_lookup.setForToken(self.state.token, self.lexer.current_code_page);
1835 // But only set the output code page to the current code page if we are past the first code_page pragma in the file.
1836 // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that
1837 // don't have an explicit output code page set.
1838 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;
1839 try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page);
1840 }
1841
1842 fn lookaheadToken(self: *Self, comptime method: Lexer.LexMethod) Error!Token {
1843 self.state.lookahead_lexer = self.lexer.*;
1844 return token: while (true) {
1845 break :token self.state.lookahead_lexer.next(method) catch |err| switch (err) {
1846 // Ignore this error and get the next valid token, we'll deal with this
1847 // properly when getting the token for real
1848 error.CodePagePragmaInIncludedFile => continue,
1849 else => return self.addErrorDetailsAndFail(self.state.lookahead_lexer.getErrorDetails(err)),
1850 };
1851 };
1852 }
1853
1854 fn tokenSlice(self: *Self) []const u8 {
1855 return self.state.token.slice(self.lexer.buffer);
1856 }
1857
1858 /// Check that the current token is something that can be used as an ID
1859 fn checkId(self: *Self) !void {
1860 switch (self.state.token.id) {
1861 .literal => {},
1862 else => {
1863 return self.addErrorDetailsAndFail(ErrorDetails{
1864 .err = .expected_token,
1865 .token = self.state.token,
1866 .extra = .{ .expected = .literal },
1867 });
1868 },
1869 }
1870 }
1871
1872 fn check(self: *Self, expected_token_id: Token.Id) !void {
1873 if (self.state.token.id != expected_token_id) {
1874 return self.addErrorDetailsAndFail(ErrorDetails{
1875 .err = .expected_token,
1876 .token = self.state.token,
1877 .extra = .{ .expected = expected_token_id },
1878 });
1879 }
1880 }
1881
1882 fn checkResource(self: *Self) !Resource {
1883 switch (self.state.token.id) {
1884 .literal => return Resource.fromString(.{
1885 .slice = self.state.token.slice(self.lexer.buffer),
1886 .code_page = self.lexer.current_code_page,
1887 }),
1888 else => {
1889 return self.addErrorDetailsAndFail(ErrorDetails{
1890 .err = .expected_token,
1891 .token = self.state.token,
1892 .extra = .{ .expected = .literal },
1893 });
1894 },
1895 }
1896 }
1897};
lib/compiler/resinator/preprocess.zig created+140
...@@ -0,0 +1,140 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const cli = @import("cli.zig");
5const aro = @import("aro");
6
7const PreprocessError = error{ ArgError, GeneratedSourceError, PreprocessError, StreamTooLong, OutOfMemory };
8
9pub fn preprocess(
10 comp: *aro.Compilation,
11 writer: anytype,
12 /// Expects argv[0] to be the command name
13 argv: []const []const u8,
14 maybe_dependencies_list: ?*std.ArrayList([]const u8),
15) PreprocessError!void {
16 try comp.addDefaultPragmaHandlers();
17
18 var driver: aro.Driver = .{ .comp = comp, .aro_name = "arocc" };
19 defer driver.deinit();
20
21 var macro_buf = std.ArrayList(u8).init(comp.gpa);
22 defer macro_buf.deinit();
23
24 _ = driver.parseArgs(std.io.null_writer, macro_buf.writer(), argv) catch |err| switch (err) {
25 error.FatalError => return error.ArgError,
26 error.OutOfMemory => |e| return e,
27 };
28
29 if (hasAnyErrors(comp)) return error.ArgError;
30
31 // .include_system_defines gives us things like _WIN32
32 const builtin_macros = comp.generateBuiltinMacros(.include_system_defines) catch |err| switch (err) {
33 error.FatalError => return error.GeneratedSourceError,
34 else => |e| return e,
35 };
36 const user_macros = comp.addSourceFromBuffer("<command line>", macro_buf.items) catch |err| switch (err) {
37 error.FatalError => return error.GeneratedSourceError,
38 else => |e| return e,
39 };
40 const source = driver.inputs.items[0];
41
42 if (hasAnyErrors(comp)) return error.GeneratedSourceError;
43
44 comp.generated_buf.items.len = 0;
45 var pp = try aro.Preprocessor.initDefault(comp);
46 defer pp.deinit();
47
48 if (comp.langopts.ms_extensions) {
49 comp.ms_cwd_source_id = source.id;
50 }
51
52 pp.preserve_whitespace = true;
53 pp.linemarkers = .line_directives;
54
55 pp.preprocessSources(&.{ source, builtin_macros, user_macros }) catch |err| switch (err) {
56 error.FatalError => return error.PreprocessError,
57 else => |e| return e,
58 };
59
60 if (hasAnyErrors(comp)) return error.PreprocessError;
61
62 try pp.prettyPrintTokens(writer);
63
64 if (maybe_dependencies_list) |dependencies_list| {
65 for (comp.sources.values()) |comp_source| {
66 if (comp_source.id == builtin_macros.id or comp_source.id == user_macros.id) continue;
67 if (comp_source.id == .unused or comp_source.id == .generated) continue;
68 const duped_path = try dependencies_list.allocator.dupe(u8, comp_source.path);
69 errdefer dependencies_list.allocator.free(duped_path);
70 try dependencies_list.append(duped_path);
71 }
72 }
73}
74
75fn hasAnyErrors(comp: *aro.Compilation) bool {
76 // In theory we could just check Diagnostics.errors != 0, but that only
77 // gets set during rendering of the error messages, see:
78 // https://github.com/Vexu/arocc/issues/603
79 for (comp.diagnostics.list.items) |msg| {
80 switch (msg.kind) {
81 .@"fatal error", .@"error" => return true,
82 else => {},
83 }
84 }
85 return false;
86}
87
88/// `arena` is used for temporary -D argument strings and the INCLUDE environment variable.
89/// The arena should be kept alive at least as long as `argv`.
90pub fn appendAroArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options: cli.Options, system_include_paths: []const []const u8) !void {
91 try argv.appendSlice(&.{
92 "-E",
93 "--comments",
94 "-fuse-line-directives",
95 "--target=x86_64-windows-msvc",
96 "--emulate=msvc",
97 "-nostdinc",
98 "-DRC_INVOKED",
99 });
100 for (options.extra_include_paths.items) |extra_include_path| {
101 try argv.append("-I");
102 try argv.append(extra_include_path);
103 }
104
105 for (system_include_paths) |include_path| {
106 try argv.append("-isystem");
107 try argv.append(include_path);
108 }
109
110 if (!options.ignore_include_env_var) {
111 const INCLUDE = std.process.getEnvVarOwned(arena, "INCLUDE") catch "";
112
113 // The only precedence here is llvm-rc which also uses the platform-specific
114 // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
115 const delimiter = switch (builtin.os.tag) {
116 .windows => ';',
117 else => ':',
118 };
119 var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
120 while (it.next()) |include_path| {
121 try argv.append("-isystem");
122 try argv.append(include_path);
123 }
124 }
125
126 var symbol_it = options.symbols.iterator();
127 while (symbol_it.next()) |entry| {
128 switch (entry.value_ptr.*) {
129 .define => |value| {
130 try argv.append("-D");
131 const define_arg = try std.fmt.allocPrint(arena, "{s}={s}", .{ entry.key_ptr.*, value });
132 try argv.append(define_arg);
133 },
134 .undefine => {
135 try argv.append("-U");
136 try argv.append(entry.key_ptr.*);
137 },
138 }
139 }
140}
lib/compiler/resinator/rc.zig created+407
...@@ -0,0 +1,407 @@
1const std = @import("std");
2const utils = @import("utils.zig");
3const res = @import("res.zig");
4const SourceBytes = @import("literals.zig").SourceBytes;
5
6// https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files
7
8pub const Resource = enum {
9 accelerators,
10 bitmap,
11 cursor,
12 dialog,
13 dialogex,
14 /// As far as I can tell, this is undocumented; the most I could find was this:
15 /// https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/91697
16 dlginclude,
17 /// Undocumented, basically works exactly like RCDATA
18 dlginit,
19 font,
20 html,
21 icon,
22 menu,
23 menuex,
24 messagetable,
25 plugplay, // Obsolete
26 rcdata,
27 stringtable,
28 /// Undocumented
29 toolbar,
30 user_defined,
31 versioninfo,
32 vxd, // Obsolete
33
34 // Types that are treated as a user-defined type when encountered, but have
35 // special meaning without the Visual Studio GUI. We match the Win32 RC compiler
36 // behavior by acting as if these keyword don't exist when compiling the .rc
37 // (thereby treating them as user-defined).
38 //textinclude, // A special resource that is interpreted by Visual C++.
39 //typelib, // A special resource that is used with the /TLBID and /TLBOUT linker options
40
41 // Types that can only be specified by numbers, they don't have keywords
42 cursor_num,
43 icon_num,
44 string_num,
45 anicursor_num,
46 aniicon_num,
47 fontdir_num,
48 manifest_num,
49
50 const map = std.ComptimeStringMapWithEql(Resource, .{
51 .{ "ACCELERATORS", .accelerators },
52 .{ "BITMAP", .bitmap },
53 .{ "CURSOR", .cursor },
54 .{ "DIALOG", .dialog },
55 .{ "DIALOGEX", .dialogex },
56 .{ "DLGINCLUDE", .dlginclude },
57 .{ "DLGINIT", .dlginit },
58 .{ "FONT", .font },
59 .{ "HTML", .html },
60 .{ "ICON", .icon },
61 .{ "MENU", .menu },
62 .{ "MENUEX", .menuex },
63 .{ "MESSAGETABLE", .messagetable },
64 .{ "PLUGPLAY", .plugplay },
65 .{ "RCDATA", .rcdata },
66 .{ "STRINGTABLE", .stringtable },
67 .{ "TOOLBAR", .toolbar },
68 .{ "VERSIONINFO", .versioninfo },
69 .{ "VXD", .vxd },
70 }, std.comptime_string_map.eqlAsciiIgnoreCase);
71
72 pub fn fromString(bytes: SourceBytes) Resource {
73 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes);
74 if (maybe_ordinal) |ordinal| {
75 if (ordinal.ordinal >= 256) return .user_defined;
76 return fromRT(@enumFromInt(ordinal.ordinal));
77 }
78 return map.get(bytes.slice) orelse .user_defined;
79 }
80
81 // TODO: Some comptime validation that RT <-> Resource conversion is synced?
82 pub fn fromRT(rt: res.RT) Resource {
83 return switch (rt) {
84 .ACCELERATOR => .accelerators,
85 .ANICURSOR => .anicursor_num,
86 .ANIICON => .aniicon_num,
87 .BITMAP => .bitmap,
88 .CURSOR => .cursor_num,
89 .DIALOG => .dialog,
90 .DLGINCLUDE => .dlginclude,
91 .DLGINIT => .dlginit,
92 .FONT => .font,
93 .FONTDIR => .fontdir_num,
94 .GROUP_CURSOR => .cursor,
95 .GROUP_ICON => .icon,
96 .HTML => .html,
97 .ICON => .icon_num,
98 .MANIFEST => .manifest_num,
99 .MENU => .menu,
100 .MESSAGETABLE => .messagetable,
101 .PLUGPLAY => .plugplay,
102 .RCDATA => .rcdata,
103 .STRING => .string_num,
104 .TOOLBAR => .toolbar,
105 .VERSION => .versioninfo,
106 .VXD => .vxd,
107 _ => .user_defined,
108 };
109 }
110
111 pub fn canUseRawData(resource: Resource) bool {
112 return switch (resource) {
113 .user_defined,
114 .html,
115 .plugplay, // Obsolete
116 .rcdata,
117 .vxd, // Obsolete
118 .manifest_num,
119 .dlginit,
120 => true,
121 else => false,
122 };
123 }
124
125 pub fn nameForErrorDisplay(resource: Resource) []const u8 {
126 return switch (resource) {
127 // zig fmt: off
128 .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font,
129 .html, .icon, .menu, .menuex, .messagetable, .plugplay, .rcdata, .stringtable,
130 .toolbar, .versioninfo, .vxd => @tagName(resource),
131 // zig fmt: on
132 .user_defined => "user-defined",
133 .cursor_num => std.fmt.comptimePrint("{d} (cursor)", .{@intFromEnum(res.RT.CURSOR)}),
134 .icon_num => std.fmt.comptimePrint("{d} (icon)", .{@intFromEnum(res.RT.ICON)}),
135 .string_num => std.fmt.comptimePrint("{d} (string)", .{@intFromEnum(res.RT.STRING)}),
136 .anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@intFromEnum(res.RT.ANICURSOR)}),
137 .aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@intFromEnum(res.RT.ANIICON)}),
138 .fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@intFromEnum(res.RT.FONTDIR)}),
139 .manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@intFromEnum(res.RT.MANIFEST)}),
140 };
141 }
142};
143
144/// https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable-resource#parameters
145/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialog-resource#parameters
146/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialogex-resource#parameters
147pub const OptionalStatements = enum {
148 characteristics,
149 language,
150 version,
151
152 // DIALOG
153 caption,
154 class,
155 exstyle,
156 font,
157 menu,
158 style,
159
160 pub const map = std.ComptimeStringMapWithEql(OptionalStatements, .{
161 .{ "CHARACTERISTICS", .characteristics },
162 .{ "LANGUAGE", .language },
163 .{ "VERSION", .version },
164 }, std.comptime_string_map.eqlAsciiIgnoreCase);
165
166 pub const dialog_map = std.ComptimeStringMapWithEql(OptionalStatements, .{
167 .{ "CAPTION", .caption },
168 .{ "CLASS", .class },
169 .{ "EXSTYLE", .exstyle },
170 .{ "FONT", .font },
171 .{ "MENU", .menu },
172 .{ "STYLE", .style },
173 }, std.comptime_string_map.eqlAsciiIgnoreCase);
174};
175
176pub const Control = enum {
177 auto3state,
178 autocheckbox,
179 autoradiobutton,
180 checkbox,
181 combobox,
182 control,
183 ctext,
184 defpushbutton,
185 edittext,
186 hedit,
187 iedit,
188 groupbox,
189 icon,
190 listbox,
191 ltext,
192 pushbox,
193 pushbutton,
194 radiobutton,
195 rtext,
196 scrollbar,
197 state3,
198 userbutton,
199
200 pub const map = std.ComptimeStringMapWithEql(Control, .{
201 .{ "AUTO3STATE", .auto3state },
202 .{ "AUTOCHECKBOX", .autocheckbox },
203 .{ "AUTORADIOBUTTON", .autoradiobutton },
204 .{ "CHECKBOX", .checkbox },
205 .{ "COMBOBOX", .combobox },
206 .{ "CONTROL", .control },
207 .{ "CTEXT", .ctext },
208 .{ "DEFPUSHBUTTON", .defpushbutton },
209 .{ "EDITTEXT", .edittext },
210 .{ "HEDIT", .hedit },
211 .{ "IEDIT", .iedit },
212 .{ "GROUPBOX", .groupbox },
213 .{ "ICON", .icon },
214 .{ "LISTBOX", .listbox },
215 .{ "LTEXT", .ltext },
216 .{ "PUSHBOX", .pushbox },
217 .{ "PUSHBUTTON", .pushbutton },
218 .{ "RADIOBUTTON", .radiobutton },
219 .{ "RTEXT", .rtext },
220 .{ "SCROLLBAR", .scrollbar },
221 .{ "STATE3", .state3 },
222 .{ "USERBUTTON", .userbutton },
223 }, std.comptime_string_map.eqlAsciiIgnoreCase);
224
225 pub fn hasTextParam(control: Control) bool {
226 switch (control) {
227 .scrollbar, .listbox, .iedit, .hedit, .edittext, .combobox => return false,
228 else => return true,
229 }
230 }
231};
232
233pub const ControlClass = struct {
234 pub const map = std.ComptimeStringMapWithEql(res.ControlClass, .{
235 .{ "BUTTON", .button },
236 .{ "EDIT", .edit },
237 .{ "STATIC", .static },
238 .{ "LISTBOX", .listbox },
239 .{ "SCROLLBAR", .scrollbar },
240 .{ "COMBOBOX", .combobox },
241 }, std.comptime_string_map.eqlAsciiIgnoreCase);
242
243 /// Like `map.get` but works on WTF16 strings, for use with parsed
244 /// string literals ("BUTTON", or even "\x42UTTON")
245 pub fn fromWideString(str: []const u16) ?res.ControlClass {
246 const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
247 return if (ascii.eqlIgnoreCaseW(str, utf16Literal("BUTTON")))
248 .button
249 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("EDIT")))
250 .edit
251 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("STATIC")))
252 .static
253 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("LISTBOX")))
254 .listbox
255 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("SCROLLBAR")))
256 .scrollbar
257 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("COMBOBOX")))
258 .combobox
259 else
260 null;
261 }
262};
263
264const ascii = struct {
265 /// Compares ASCII values case-insensitively, non-ASCII values are compared directly
266 pub fn eqlIgnoreCaseW(a: []const u16, b: []const u16) bool {
267 if (a.len != b.len) return false;
268 for (a, b) |a_c, b_c| {
269 if (a_c < 128) {
270 if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false;
271 } else {
272 if (a_c != b_c) return false;
273 }
274 }
275 return true;
276 }
277};
278
279pub const MenuItem = enum {
280 menuitem,
281 popup,
282
283 pub const map = std.ComptimeStringMapWithEql(MenuItem, .{
284 .{ "MENUITEM", .menuitem },
285 .{ "POPUP", .popup },
286 }, std.comptime_string_map.eqlAsciiIgnoreCase);
287
288 pub fn isSeparator(bytes: []const u8) bool {
289 return std.ascii.eqlIgnoreCase(bytes, "SEPARATOR");
290 }
291
292 pub const Option = enum {
293 checked,
294 grayed,
295 help,
296 inactive,
297 menubarbreak,
298 menubreak,
299
300 pub const map = std.ComptimeStringMapWithEql(Option, .{
301 .{ "CHECKED", .checked },
302 .{ "GRAYED", .grayed },
303 .{ "HELP", .help },
304 .{ "INACTIVE", .inactive },
305 .{ "MENUBARBREAK", .menubarbreak },
306 .{ "MENUBREAK", .menubreak },
307 }, std.comptime_string_map.eqlAsciiIgnoreCase);
308 };
309};
310
311pub const ToolbarButton = enum {
312 button,
313 separator,
314
315 pub const map = std.ComptimeStringMapWithEql(ToolbarButton, .{
316 .{ "BUTTON", .button },
317 .{ "SEPARATOR", .separator },
318 }, std.comptime_string_map.eqlAsciiIgnoreCase);
319};
320
321pub const VersionInfo = enum {
322 file_version,
323 product_version,
324 file_flags_mask,
325 file_flags,
326 file_os,
327 file_type,
328 file_subtype,
329
330 pub const map = std.ComptimeStringMapWithEql(VersionInfo, .{
331 .{ "FILEVERSION", .file_version },
332 .{ "PRODUCTVERSION", .product_version },
333 .{ "FILEFLAGSMASK", .file_flags_mask },
334 .{ "FILEFLAGS", .file_flags },
335 .{ "FILEOS", .file_os },
336 .{ "FILETYPE", .file_type },
337 .{ "FILESUBTYPE", .file_subtype },
338 }, std.comptime_string_map.eqlAsciiIgnoreCase);
339};
340
341pub const VersionBlock = enum {
342 block,
343 value,
344
345 pub const map = std.ComptimeStringMapWithEql(VersionBlock, .{
346 .{ "BLOCK", .block },
347 .{ "VALUE", .value },
348 }, std.comptime_string_map.eqlAsciiIgnoreCase);
349};
350
351/// Keywords that are be the first token in a statement and (if so) dictate how the rest
352/// of the statement is parsed.
353pub const TopLevelKeywords = enum {
354 language,
355 version,
356 characteristics,
357 stringtable,
358
359 pub const map = std.ComptimeStringMapWithEql(TopLevelKeywords, .{
360 .{ "LANGUAGE", .language },
361 .{ "VERSION", .version },
362 .{ "CHARACTERISTICS", .characteristics },
363 .{ "STRINGTABLE", .stringtable },
364 }, std.comptime_string_map.eqlAsciiIgnoreCase);
365};
366
367pub const CommonResourceAttributes = enum {
368 preload,
369 loadoncall,
370 fixed,
371 moveable,
372 discardable,
373 pure,
374 impure,
375 shared,
376 nonshared,
377
378 pub const map = std.ComptimeStringMapWithEql(CommonResourceAttributes, .{
379 .{ "PRELOAD", .preload },
380 .{ "LOADONCALL", .loadoncall },
381 .{ "FIXED", .fixed },
382 .{ "MOVEABLE", .moveable },
383 .{ "DISCARDABLE", .discardable },
384 .{ "PURE", .pure },
385 .{ "IMPURE", .impure },
386 .{ "SHARED", .shared },
387 .{ "NONSHARED", .nonshared },
388 }, std.comptime_string_map.eqlAsciiIgnoreCase);
389};
390
391pub const AcceleratorTypeAndOptions = enum {
392 virtkey,
393 ascii,
394 noinvert,
395 alt,
396 shift,
397 control,
398
399 pub const map = std.ComptimeStringMapWithEql(AcceleratorTypeAndOptions, .{
400 .{ "VIRTKEY", .virtkey },
401 .{ "ASCII", .ascii },
402 .{ "NOINVERT", .noinvert },
403 .{ "ALT", .alt },
404 .{ "SHIFT", .shift },
405 .{ "CONTROL", .control },
406 }, std.comptime_string_map.eqlAsciiIgnoreCase);
407};
lib/compiler/resinator/res.zig created+1107
...@@ -0,0 +1,1107 @@
1const std = @import("std");
2const rc = @import("rc.zig");
3const Resource = rc.Resource;
4const CommonResourceAttributes = rc.CommonResourceAttributes;
5const Allocator = std.mem.Allocator;
6const windows1252 = @import("windows1252.zig");
7const CodePage = @import("code_pages.zig").CodePage;
8const literals = @import("literals.zig");
9const SourceBytes = literals.SourceBytes;
10const Codepoint = @import("code_pages.zig").Codepoint;
11const lang = @import("lang.zig");
12const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
13
14/// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
15pub const RT = enum(u8) {
16 ACCELERATOR = 9,
17 ANICURSOR = 21,
18 ANIICON = 22,
19 BITMAP = 2,
20 CURSOR = 1,
21 DIALOG = 5,
22 DLGINCLUDE = 17,
23 DLGINIT = 240,
24 FONT = 8,
25 FONTDIR = 7,
26 GROUP_CURSOR = 1 + 11, // CURSOR + 11
27 GROUP_ICON = 3 + 11, // ICON + 11
28 HTML = 23,
29 ICON = 3,
30 MANIFEST = 24,
31 MENU = 4,
32 MESSAGETABLE = 11,
33 PLUGPLAY = 19,
34 RCDATA = 10,
35 STRING = 6,
36 TOOLBAR = 241,
37 VERSION = 16,
38 VXD = 20,
39 _,
40
41 /// Returns null if the resource type is user-defined
42 /// Asserts that the resource is not `stringtable`
43 pub fn fromResource(resource: Resource) ?RT {
44 return switch (resource) {
45 .accelerators => .ACCELERATOR,
46 .bitmap => .BITMAP,
47 .cursor => .GROUP_CURSOR,
48 .dialog => .DIALOG,
49 .dialogex => .DIALOG,
50 .dlginclude => .DLGINCLUDE,
51 .dlginit => .DLGINIT,
52 .font => .FONT,
53 .html => .HTML,
54 .icon => .GROUP_ICON,
55 .menu => .MENU,
56 .menuex => .MENU,
57 .messagetable => .MESSAGETABLE,
58 .plugplay => .PLUGPLAY,
59 .rcdata => .RCDATA,
60 .stringtable => unreachable,
61 .toolbar => .TOOLBAR,
62 .user_defined => null,
63 .versioninfo => .VERSION,
64 .vxd => .VXD,
65
66 .cursor_num => .CURSOR,
67 .icon_num => .ICON,
68 .string_num => .STRING,
69 .anicursor_num => .ANICURSOR,
70 .aniicon_num => .ANIICON,
71 .fontdir_num => .FONTDIR,
72 .manifest_num => .MANIFEST,
73 };
74 }
75};
76
77/// https://learn.microsoft.com/en-us/windows/win32/menurc/common-resource-attributes
78/// https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader
79pub const MemoryFlags = packed struct(u16) {
80 value: u16,
81
82 pub const MOVEABLE: u16 = 0x10;
83 // TODO: SHARED and PURE seem to be the same thing? Testing seems to confirm this but
84 // would like to find mention of it somewhere.
85 pub const SHARED: u16 = 0x20;
86 pub const PURE: u16 = 0x20;
87 pub const PRELOAD: u16 = 0x40;
88 pub const DISCARDABLE: u16 = 0x1000;
89
90 /// Note: The defaults can have combinations that are not possible to specify within
91 /// an .rc file, as the .rc attributes imply other values (i.e. specifying
92 /// DISCARDABLE always implies MOVEABLE and PURE/SHARED, and yet RT_ICON
93 /// has a default of only MOVEABLE | DISCARDABLE).
94 pub fn defaults(predefined_resource_type: ?RT) MemoryFlags {
95 if (predefined_resource_type == null) {
96 return MemoryFlags{ .value = MOVEABLE | SHARED };
97 } else {
98 return switch (predefined_resource_type.?) {
99 // zig fmt: off
100 .RCDATA, .BITMAP, .HTML, .MANIFEST,
101 .ACCELERATOR, .VERSION, .MESSAGETABLE,
102 .DLGINIT, .TOOLBAR, .PLUGPLAY,
103 .VXD, => MemoryFlags{ .value = MOVEABLE | SHARED },
104
105 .GROUP_ICON, .GROUP_CURSOR,
106 .STRING, .FONT, .DIALOG, .MENU,
107 .DLGINCLUDE, => MemoryFlags{ .value = MOVEABLE | SHARED | DISCARDABLE },
108
109 .ICON, .CURSOR, .ANIICON, .ANICURSOR => MemoryFlags{ .value = MOVEABLE | DISCARDABLE },
110 .FONTDIR => MemoryFlags{ .value = MOVEABLE | PRELOAD },
111 // zig fmt: on
112 // Same as predefined_resource_type == null
113 _ => return MemoryFlags{ .value = MOVEABLE | SHARED },
114 };
115 }
116 }
117
118 pub fn set(self: *MemoryFlags, attribute: CommonResourceAttributes) void {
119 switch (attribute) {
120 .preload => self.value |= PRELOAD,
121 .loadoncall => self.value &= ~PRELOAD,
122 .moveable => self.value |= MOVEABLE,
123 .fixed => self.value &= ~(MOVEABLE | DISCARDABLE),
124 .shared => self.value |= SHARED,
125 .nonshared => self.value &= ~(SHARED | DISCARDABLE),
126 .pure => self.value |= PURE,
127 .impure => self.value &= ~(PURE | DISCARDABLE),
128 .discardable => self.value |= DISCARDABLE | MOVEABLE | PURE,
129 }
130 }
131
132 pub fn setGroup(self: *MemoryFlags, attribute: CommonResourceAttributes, implied_shared_or_pure: bool) void {
133 switch (attribute) {
134 .preload => {
135 self.value |= PRELOAD;
136 if (implied_shared_or_pure) self.value &= ~SHARED;
137 },
138 .loadoncall => {
139 self.value &= ~PRELOAD;
140 if (implied_shared_or_pure) self.value |= SHARED;
141 },
142 else => self.set(attribute),
143 }
144 }
145};
146
147/// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers
148pub const Language = packed struct(u16) {
149 // Note: This is the default no matter what locale the current system is set to,
150 // e.g. even if the system's locale is en-GB, en-US will still be the
151 // default language for resources in the Win32 rc compiler.
152 primary_language_id: u10 = lang.LANG_ENGLISH,
153 sublanguage_id: u6 = lang.SUBLANG_ENGLISH_US,
154
155 /// Default language ID as a u16
156 pub const default: u16 = (Language{}).asInt();
157
158 pub fn fromInt(int: u16) Language {
159 return @bitCast(int);
160 }
161
162 pub fn asInt(self: Language) u16 {
163 return @bitCast(self);
164 }
165};
166
167/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks
168pub const ControlClass = enum(u16) {
169 button = 0x80,
170 edit = 0x81,
171 static = 0x82,
172 listbox = 0x83,
173 scrollbar = 0x84,
174 combobox = 0x85,
175
176 pub fn fromControl(control: rc.Control) ?ControlClass {
177 return switch (control) {
178 // zig fmt: off
179 .auto3state, .autocheckbox, .autoradiobutton,
180 .checkbox, .defpushbutton, .groupbox, .pushbox,
181 .pushbutton, .radiobutton, .state3, .userbutton => .button,
182 // zig fmt: on
183 .combobox => .combobox,
184 .control => null,
185 .ctext, .icon, .ltext, .rtext => .static,
186 .edittext, .hedit, .iedit => .edit,
187 .listbox => .listbox,
188 .scrollbar => .scrollbar,
189 };
190 }
191
192 pub fn getImpliedStyle(control: rc.Control) u32 {
193 var style = WS.CHILD | WS.VISIBLE;
194 switch (control) {
195 .auto3state => style |= BS.AUTO3STATE | WS.TABSTOP,
196 .autocheckbox => style |= BS.AUTOCHECKBOX | WS.TABSTOP,
197 .autoradiobutton => style |= BS.AUTORADIOBUTTON,
198 .checkbox => style |= BS.CHECKBOX | WS.TABSTOP,
199 .combobox => {},
200 .control => {},
201 .ctext => style |= SS.CENTER | WS.GROUP,
202 .defpushbutton => style |= BS.DEFPUSHBUTTON | WS.TABSTOP,
203 .edittext, .hedit, .iedit => style |= WS.TABSTOP | WS.BORDER,
204 .groupbox => style |= BS.GROUPBOX,
205 .icon => style |= SS.ICON,
206 .listbox => style |= LBS.NOTIFY | WS.BORDER,
207 .ltext => style |= WS.GROUP,
208 .pushbox => style |= BS.PUSHBOX | WS.TABSTOP,
209 .pushbutton => style |= WS.TABSTOP,
210 .radiobutton => style |= BS.RADIOBUTTON,
211 .rtext => style |= SS.RIGHT | WS.GROUP,
212 .scrollbar => {},
213 .state3 => style |= BS.@"3STATE" | WS.TABSTOP,
214 .userbutton => style |= BS.USERBUTTON | WS.TABSTOP,
215 }
216 return style;
217 }
218};
219
220pub const NameOrOrdinal = union(enum) {
221 // UTF-16 LE
222 name: [:0]const u16,
223 ordinal: u16,
224
225 pub fn deinit(self: NameOrOrdinal, allocator: Allocator) void {
226 switch (self) {
227 .name => |name| {
228 allocator.free(name);
229 },
230 .ordinal => {},
231 }
232 }
233
234 /// Returns the full length of the amount of bytes that would be written by `write`
235 /// (e.g. for an ordinal it will return the length including the 0xFFFF indicator)
236 pub fn byteLen(self: NameOrOrdinal) usize {
237 switch (self) {
238 .name => |name| {
239 // + 1 for 0-terminated
240 return (name.len + 1) * @sizeOf(u16);
241 },
242 .ordinal => return 4,
243 }
244 }
245
246 pub fn write(self: NameOrOrdinal, writer: anytype) !void {
247 switch (self) {
248 .name => |name| {
249 try writer.writeAll(std.mem.sliceAsBytes(name[0 .. name.len + 1]));
250 },
251 .ordinal => |ordinal| {
252 try writer.writeInt(u16, 0xffff, .little);
253 try writer.writeInt(u16, ordinal, .little);
254 },
255 }
256 }
257
258 pub fn writeEmpty(writer: anytype) !void {
259 try writer.writeInt(u16, 0, .little);
260 }
261
262 pub fn fromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
263 if (maybeOrdinalFromString(bytes)) |ordinal| {
264 return ordinal;
265 }
266 return nameFromString(allocator, bytes);
267 }
268
269 pub fn nameFromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
270 // Names have a limit of 256 UTF-16 code units + null terminator
271 var buf = try std.ArrayList(u16).initCapacity(allocator, @min(257, bytes.slice.len));
272 errdefer buf.deinit();
273
274 var i: usize = 0;
275 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
276 if (buf.items.len == 256) break;
277
278 const c = codepoint.value;
279 if (c == Codepoint.invalid) {
280 try buf.append(std.mem.nativeToLittle(u16, '�'));
281 } else if (c < 0x7F) {
282 // ASCII chars in names are always converted to uppercase
283 try buf.append(std.mem.nativeToLittle(u16, std.ascii.toUpper(@intCast(c))));
284 } else if (c < 0x10000) {
285 const short: u16 = @intCast(c);
286 try buf.append(std.mem.nativeToLittle(u16, short));
287 } else {
288 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
289 try buf.append(std.mem.nativeToLittle(u16, high));
290
291 // Note: This can cut-off in the middle of a UTF-16 surrogate pair,
292 // i.e. it can make the string end with an unpaired high surrogate
293 if (buf.items.len == 256) break;
294
295 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
296 try buf.append(std.mem.nativeToLittle(u16, low));
297 }
298 }
299
300 return NameOrOrdinal{ .name = try buf.toOwnedSliceSentinel(0) };
301 }
302
303 /// Returns `null` if the bytes do not form a valid number.
304 /// Does not allow non-ASCII digits (which the Win32 RC compiler does allow
305 /// in base 10 numbers, see `maybeNonAsciiOrdinalFromString`).
306 pub fn maybeOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
307 var buf = bytes.slice;
308 var radix: u8 = 10;
309 if (buf.len > 2 and buf[0] == '0') {
310 switch (buf[1]) {
311 '0'...'9' => {},
312 'x', 'X' => {
313 radix = 16;
314 buf = buf[2..];
315 // only the first 4 hex digits matter, anything else is ignored
316 // i.e. 0x12345 is treated as if it were 0x1234
317 buf.len = @min(buf.len, 4);
318 },
319 else => return null,
320 }
321 }
322
323 var i: usize = 0;
324 var result: u16 = 0;
325 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
326 const c = codepoint.value;
327 const digit: u8 = switch (c) {
328 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch switch (radix) {
329 10 => return null,
330 // non-hex-digits are treated as a terminator rather than invalidating
331 // the number (note: if there are no valid hex digits then the result
332 // will be zero which is not treated as a valid number)
333 16 => break,
334 else => unreachable,
335 },
336 else => if (radix == 10) return null else break,
337 };
338
339 if (result != 0) {
340 result *%= radix;
341 }
342 result +%= digit;
343 }
344
345 // Anything that resolves to zero is not interpretted as a number
346 if (result == 0) return null;
347 return NameOrOrdinal{ .ordinal = result };
348 }
349
350 /// The Win32 RC compiler uses `iswdigit` for digit detection for base 10
351 /// numbers, which means that non-ASCII digits are 'accepted' but handled
352 /// in a totally unintuitive manner, leading to arbitrary results.
353 ///
354 /// This function will return the value that such an ordinal 'would' have
355 /// if it was run through the Win32 RC compiler. This allows us to disallow
356 /// non-ASCII digits in number literals but still detect when the Win32
357 /// RC compiler would have allowed them, so that a proper warning/error
358 /// can be emitted.
359 pub fn maybeNonAsciiOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
360 const buf = bytes.slice;
361 const radix = 10;
362 if (buf.len > 2 and buf[0] == '0') {
363 switch (buf[1]) {
364 // We only care about base 10 numbers here
365 'x', 'X' => return null,
366 else => {},
367 }
368 }
369
370 var i: usize = 0;
371 var result: u16 = 0;
372 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
373 const c = codepoint.value;
374 const digit: u16 = digit: {
375 const is_digit = (c >= '0' and c <= '9') or isNonAsciiDigit(c);
376 if (!is_digit) return null;
377 break :digit @intCast(c - '0');
378 };
379
380 if (result != 0) {
381 result *%= radix;
382 }
383 result +%= digit;
384 }
385
386 // Anything that resolves to zero is not interpretted as a number
387 if (result == 0) return null;
388 return NameOrOrdinal{ .ordinal = result };
389 }
390
391 pub fn predefinedResourceType(self: NameOrOrdinal) ?RT {
392 switch (self) {
393 .ordinal => |ordinal| {
394 if (ordinal >= 256) return null;
395 switch (@as(RT, @enumFromInt(ordinal))) {
396 .ACCELERATOR,
397 .ANICURSOR,
398 .ANIICON,
399 .BITMAP,
400 .CURSOR,
401 .DIALOG,
402 .DLGINCLUDE,
403 .DLGINIT,
404 .FONT,
405 .FONTDIR,
406 .GROUP_CURSOR,
407 .GROUP_ICON,
408 .HTML,
409 .ICON,
410 .MANIFEST,
411 .MENU,
412 .MESSAGETABLE,
413 .PLUGPLAY,
414 .RCDATA,
415 .STRING,
416 .TOOLBAR,
417 .VERSION,
418 .VXD,
419 => |rt| return rt,
420 _ => return null,
421 }
422 },
423 .name => return null,
424 }
425 }
426};
427
428fn expectNameOrOrdinal(expected: NameOrOrdinal, actual: NameOrOrdinal) !void {
429 switch (expected) {
430 .name => {
431 if (actual != .name) return error.TestExpectedEqual;
432 try std.testing.expectEqualSlices(u16, expected.name, actual.name);
433 },
434 .ordinal => {
435 if (actual != .ordinal) return error.TestExpectedEqual;
436 try std.testing.expectEqual(expected.ordinal, actual.ordinal);
437 },
438 }
439}
440
441test "NameOrOrdinal" {
442 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
443 defer arena.deinit();
444
445 const allocator = arena.allocator();
446
447 // zero is treated as a string
448 try expectNameOrOrdinal(
449 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0") },
450 try NameOrOrdinal.fromString(allocator, .{ .slice = "0", .code_page = .windows1252 }),
451 );
452 // any non-digit byte invalidates the number
453 try expectNameOrOrdinal(
454 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1A") },
455 try NameOrOrdinal.fromString(allocator, .{ .slice = "1a", .code_page = .windows1252 }),
456 );
457 try expectNameOrOrdinal(
458 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1ÿ") },
459 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\xff", .code_page = .windows1252 }),
460 );
461 try expectNameOrOrdinal(
462 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1€") },
463 try NameOrOrdinal.fromString(allocator, .{ .slice = "1€", .code_page = .utf8 }),
464 );
465 try expectNameOrOrdinal(
466 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1�") },
467 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\x80", .code_page = .utf8 }),
468 );
469 // same with overflow that resolves to 0
470 try expectNameOrOrdinal(
471 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("65536") },
472 try NameOrOrdinal.fromString(allocator, .{ .slice = "65536", .code_page = .windows1252 }),
473 );
474 // hex zero is also treated as a string
475 try expectNameOrOrdinal(
476 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0X0") },
477 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x0", .code_page = .windows1252 }),
478 );
479 // hex numbers work
480 try expectNameOrOrdinal(
481 NameOrOrdinal{ .ordinal = 0x100 },
482 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x100", .code_page = .windows1252 }),
483 );
484 // only the first 4 hex digits matter
485 try expectNameOrOrdinal(
486 NameOrOrdinal{ .ordinal = 0x1234 },
487 try NameOrOrdinal.fromString(allocator, .{ .slice = "0X12345", .code_page = .windows1252 }),
488 );
489 // octal is not supported so it gets treated as a string
490 try expectNameOrOrdinal(
491 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0O1234") },
492 try NameOrOrdinal.fromString(allocator, .{ .slice = "0o1234", .code_page = .windows1252 }),
493 );
494 // overflow wraps
495 try expectNameOrOrdinal(
496 NameOrOrdinal{ .ordinal = @truncate(65635) },
497 try NameOrOrdinal.fromString(allocator, .{ .slice = "65635", .code_page = .windows1252 }),
498 );
499 // non-hex-digits in a hex literal are treated as a terminator
500 try expectNameOrOrdinal(
501 NameOrOrdinal{ .ordinal = 0x4 },
502 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x4n", .code_page = .windows1252 }),
503 );
504 try expectNameOrOrdinal(
505 NameOrOrdinal{ .ordinal = 0xFA },
506 try NameOrOrdinal.fromString(allocator, .{ .slice = "0xFAZ92348", .code_page = .windows1252 }),
507 );
508 // 0 at the start is allowed
509 try expectNameOrOrdinal(
510 NameOrOrdinal{ .ordinal = 50 },
511 try NameOrOrdinal.fromString(allocator, .{ .slice = "050", .code_page = .windows1252 }),
512 );
513 // limit of 256 UTF-16 code units, can cut off between a surrogate pair
514 {
515 var expected = blk: {
516 // the input before the 𐐷 character, but uppercased
517 const expected_u8_bytes = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528QFFL7SHNSIETG0QKLR1UYPBTUV1PMFQRRA0VJDG354GQEDJMUPGPP1W1EXVNTZVEIZ6K3IPQM1AWGEYALMEODYVEZGOD3MFMGEY8FNR4JUETTB1PZDEWSNDRGZUA8SNXP3NGO";
518 var buf: [256:0]u16 = undefined;
519 for (expected_u8_bytes, 0..) |byte, i| {
520 buf[i] = std.mem.nativeToLittle(u16, byte);
521 }
522 // surrogate pair that is now orphaned
523 buf[255] = std.mem.nativeToLittle(u16, 0xD801);
524 break :blk buf;
525 };
526 try expectNameOrOrdinal(
527 NameOrOrdinal{ .name = &expected },
528 try NameOrOrdinal.fromString(allocator, .{
529 .slice = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528qffL7ShnSIETg0qkLr1UYpbtuv1PMFQRRa0VjDG354GQedJmUPgpp1w1ExVnTzVEiz6K3iPqM1AWGeYALmeODyvEZGOD3MfmGey8fnR4jUeTtB1PzdeWsNDrGzuA8Snxp3NGO𐐷",
530 .code_page = .utf8,
531 }),
532 );
533 }
534}
535
536test "NameOrOrdinal code page awareness" {
537 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
538 defer arena.deinit();
539
540 const allocator = arena.allocator();
541
542 try expectNameOrOrdinal(
543 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("��𐐷") },
544 try NameOrOrdinal.fromString(allocator, .{
545 .slice = "\xF0\x80\x80𐐷",
546 .code_page = .utf8,
547 }),
548 );
549 try expectNameOrOrdinal(
550 // The UTF-8 representation of 𐐷 is 0xF0 0x90 0x90 0xB7. In order to provide valid
551 // UTF-8 to utf8ToUtf16LeStringLiteral, it uses the UTF-8 representation of the codepoint
552 // <U+0x90> which is 0xC2 0x90. The code units in the expected UTF-16 string are:
553 // { 0x00F0, 0x20AC, 0x20AC, 0x00F0, 0x0090, 0x0090, 0x00B7 }
554 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("ð€€ð\xC2\x90\xC2\x90·") },
555 try NameOrOrdinal.fromString(allocator, .{
556 .slice = "\xF0\x80\x80𐐷",
557 .code_page = .windows1252,
558 }),
559 );
560}
561
562/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-accel#members
563/// https://devblogs.microsoft.com/oldnewthing/20070316-00/?p=27593
564pub const AcceleratorModifiers = struct {
565 value: u8 = 0,
566 explicit_ascii_or_virtkey: bool = false,
567
568 pub const ASCII = 0;
569 pub const VIRTKEY = 1;
570 pub const NOINVERT = 1 << 1;
571 pub const SHIFT = 1 << 2;
572 pub const CONTROL = 1 << 3;
573 pub const ALT = 1 << 4;
574 /// Marker for the last accelerator in an accelerator table
575 pub const last_accelerator_in_table = 1 << 7;
576
577 pub fn apply(self: *AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) void {
578 if (modifier == .ascii or modifier == .virtkey) self.explicit_ascii_or_virtkey = true;
579 self.value |= modifierValue(modifier);
580 }
581
582 pub fn isSet(self: AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) bool {
583 // ASCII is set whenever VIRTKEY is not
584 if (modifier == .ascii) return self.value & modifierValue(.virtkey) == 0;
585 return self.value & modifierValue(modifier) != 0;
586 }
587
588 fn modifierValue(modifier: rc.AcceleratorTypeAndOptions) u8 {
589 return switch (modifier) {
590 .ascii => ASCII,
591 .virtkey => VIRTKEY,
592 .noinvert => NOINVERT,
593 .shift => SHIFT,
594 .control => CONTROL,
595 .alt => ALT,
596 };
597 }
598
599 pub fn markLast(self: *AcceleratorModifiers) void {
600 self.value |= last_accelerator_in_table;
601 }
602};
603
604const AcceleratorKeyCodepointTranslator = struct {
605 string_type: literals.StringType,
606
607 pub fn translate(self: @This(), maybe_parsed: ?literals.IterativeStringParser.ParsedCodepoint) ?u21 {
608 const parsed = maybe_parsed orelse return null;
609 if (parsed.codepoint == Codepoint.invalid) return 0xFFFD;
610 if (parsed.from_escaped_integer and self.string_type == .ascii) {
611 return windows1252.toCodepoint(@truncate(parsed.codepoint));
612 }
613 return parsed.codepoint;
614 }
615};
616
617pub const ParseAcceleratorKeyStringError = error{ EmptyAccelerator, AcceleratorTooLong, InvalidControlCharacter, ControlCharacterOutOfRange };
618
619/// Expects bytes to be the full bytes of a string literal token (e.g. including the "" or L"").
620pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: literals.StringParseOptions) (ParseAcceleratorKeyStringError || Allocator.Error)!u16 {
621 if (bytes.slice.len == 0) {
622 return error.EmptyAccelerator;
623 }
624
625 var parser = literals.IterativeStringParser.init(bytes, options);
626 var translator = AcceleratorKeyCodepointTranslator{ .string_type = parser.declared_string_type };
627
628 const first_codepoint = translator.translate(try parser.next()) orelse return error.EmptyAccelerator;
629 // 0 is treated as a terminator, so this is equivalent to an empty string
630 if (first_codepoint == 0) return error.EmptyAccelerator;
631
632 if (first_codepoint == '^') {
633 // Note: Emitting this warning unconditonally whenever ^ is the first character
634 // matches the Win32 RC behavior, but it's questionable whether or not
635 // the warning should be emitted for ^^ since that results in the ASCII
636 // character ^ being written to the .res.
637 if (is_virt and options.diagnostics != null) {
638 try options.diagnostics.?.diagnostics.append(.{
639 .err = .ascii_character_not_equivalent_to_virtual_key_code,
640 .type = .warning,
641 .token = options.diagnostics.?.token,
642 });
643 }
644
645 const c = translator.translate(try parser.next()) orelse return error.InvalidControlCharacter;
646 switch (c) {
647 '^' => return '^', // special case
648 'a'...'z', 'A'...'Z' => return std.ascii.toUpper(@intCast(c)) - 0x40,
649 // Note: The Windows RC compiler allows more than just A-Z, but what it allows
650 // seems to be tied to some sort of Unicode-aware 'is character' function or something.
651 // The full list of codepoints that trigger an out-of-range error can be found here:
652 // https://gist.github.com/squeek502/2e9d0a4728a83eed074ad9785a209fd0
653 // For codepoints >= 0x80 that don't trigger the error, the Windows RC compiler takes the
654 // codepoint and does the `- 0x40` transformation as if it were A-Z which couldn't lead
655 // to anything useable, so there's no point in emulating that behavior--erroring for
656 // all non-[a-zA-Z] makes much more sense and is what was probably intended by the
657 // Windows RC compiler.
658 else => return error.ControlCharacterOutOfRange,
659 }
660 @compileError("this should be unreachable");
661 }
662
663 const second_codepoint = translator.translate(try parser.next());
664
665 var result: u32 = initial_value: {
666 if (first_codepoint >= 0x10000) {
667 if (second_codepoint != null and second_codepoint.? != 0) return error.AcceleratorTooLong;
668 // No idea why it works this way, but this seems to match the Windows RC
669 // behavior for codepoints >= 0x10000
670 const low = @as(u16, @intCast(first_codepoint & 0x3FF)) + 0xDC00;
671 const extra = (first_codepoint - 0x10000) / 0x400;
672 break :initial_value low + extra * 0x100;
673 }
674 break :initial_value first_codepoint;
675 };
676
677 // 0 is treated as a terminator
678 if (second_codepoint != null and second_codepoint.? == 0) return @truncate(result);
679
680 const third_codepoint = translator.translate(try parser.next());
681 // 0 is treated as a terminator, so a 0 in the third position is fine but
682 // anything else is too many codepoints for an accelerator
683 if (third_codepoint != null and third_codepoint.? != 0) return error.AcceleratorTooLong;
684
685 if (second_codepoint) |c| {
686 if (c >= 0x10000) return error.AcceleratorTooLong;
687 result <<= 8;
688 result += c;
689 } else if (is_virt) {
690 switch (result) {
691 'a'...'z' => result -= 0x20, // toUpper
692 else => {},
693 }
694 }
695 return @truncate(result);
696}
697
698test "accelerator keys" {
699 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
700 .{ .slice = "\"^a\"", .code_page = .windows1252 },
701 false,
702 .{},
703 ));
704 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
705 .{ .slice = "\"^A\"", .code_page = .windows1252 },
706 false,
707 .{},
708 ));
709 try std.testing.expectEqual(@as(u16, 26), try parseAcceleratorKeyString(
710 .{ .slice = "\"^Z\"", .code_page = .windows1252 },
711 false,
712 .{},
713 ));
714 try std.testing.expectEqual(@as(u16, '^'), try parseAcceleratorKeyString(
715 .{ .slice = "\"^^\"", .code_page = .windows1252 },
716 false,
717 .{},
718 ));
719
720 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
721 .{ .slice = "\"a\"", .code_page = .windows1252 },
722 false,
723 .{},
724 ));
725 try std.testing.expectEqual(@as(u16, 0x6162), try parseAcceleratorKeyString(
726 .{ .slice = "\"ab\"", .code_page = .windows1252 },
727 false,
728 .{},
729 ));
730
731 try std.testing.expectEqual(@as(u16, 'C'), try parseAcceleratorKeyString(
732 .{ .slice = "\"c\"", .code_page = .windows1252 },
733 true,
734 .{},
735 ));
736 try std.testing.expectEqual(@as(u16, 0x6363), try parseAcceleratorKeyString(
737 .{ .slice = "\"cc\"", .code_page = .windows1252 },
738 true,
739 .{},
740 ));
741
742 // \x00 or any escape that evaluates to zero acts as a terminator, everything past it
743 // is ignored
744 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
745 .{ .slice = "\"a\\0bcdef\"", .code_page = .windows1252 },
746 false,
747 .{},
748 ));
749
750 // \x80 is € in Windows-1252, which is Unicode codepoint 20AC
751 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
752 .{ .slice = "\"\x80\"", .code_page = .windows1252 },
753 false,
754 .{},
755 ));
756 // This depends on the code page, though, with codepage 65001, \x80
757 // on its own is invalid UTF-8 so it gets converted to the replacement character
758 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
759 .{ .slice = "\"\x80\"", .code_page = .utf8 },
760 false,
761 .{},
762 ));
763 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
764 .{ .slice = "\"\x80\x80\"", .code_page = .windows1252 },
765 false,
766 .{},
767 ));
768 // This also behaves the same with escaped characters
769 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
770 .{ .slice = "\"\\x80\"", .code_page = .windows1252 },
771 false,
772 .{},
773 ));
774 // Even with utf8 code page
775 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
776 .{ .slice = "\"\\x80\"", .code_page = .utf8 },
777 false,
778 .{},
779 ));
780 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
781 .{ .slice = "\"\\x80\\x80\"", .code_page = .windows1252 },
782 false,
783 .{},
784 ));
785 // Wide string with the actual characters behaves like the ASCII string version
786 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
787 .{ .slice = "L\"\x80\x80\"", .code_page = .windows1252 },
788 false,
789 .{},
790 ));
791 // But wide string with escapes behaves differently
792 try std.testing.expectEqual(@as(u16, 0x8080), try parseAcceleratorKeyString(
793 .{ .slice = "L\"\\x80\\x80\"", .code_page = .windows1252 },
794 false,
795 .{},
796 ));
797 // and invalid escapes within wide strings get skipped
798 try std.testing.expectEqual(@as(u16, 'z'), try parseAcceleratorKeyString(
799 .{ .slice = "L\"\\Hz\"", .code_page = .windows1252 },
800 false,
801 .{},
802 ));
803
804 // any non-A-Z codepoints are illegal
805 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
806 .{ .slice = "\"^\x83\"", .code_page = .windows1252 },
807 false,
808 .{},
809 ));
810 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
811 .{ .slice = "\"^1\"", .code_page = .windows1252 },
812 false,
813 .{},
814 ));
815 try std.testing.expectError(error.InvalidControlCharacter, parseAcceleratorKeyString(
816 .{ .slice = "\"^\"", .code_page = .windows1252 },
817 false,
818 .{},
819 ));
820 try std.testing.expectError(error.EmptyAccelerator, parseAcceleratorKeyString(
821 .{ .slice = "\"\"", .code_page = .windows1252 },
822 false,
823 .{},
824 ));
825 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
826 .{ .slice = "\"hello\"", .code_page = .windows1252 },
827 false,
828 .{},
829 ));
830 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
831 .{ .slice = "\"^\x80\"", .code_page = .windows1252 },
832 false,
833 .{},
834 ));
835
836 // Invalid UTF-8 gets converted to 0xFFFD, multiple invalids get shifted and added together
837 // The behavior is the same for ascii and wide strings
838 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
839 .{ .slice = "\"\x80\x80\"", .code_page = .utf8 },
840 false,
841 .{},
842 ));
843 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
844 .{ .slice = "L\"\x80\x80\"", .code_page = .utf8 },
845 false,
846 .{},
847 ));
848
849 // Codepoints >= 0x10000
850 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
851 .{ .slice = "\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
852 false,
853 .{},
854 ));
855 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
856 .{ .slice = "L\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
857 false,
858 .{},
859 ));
860 try std.testing.expectEqual(@as(u16, 0x9C01), try parseAcceleratorKeyString(
861 .{ .slice = "\"\xF4\x80\x80\x81\"", .code_page = .utf8 },
862 false,
863 .{},
864 ));
865 // anything before or after a codepoint >= 0x10000 causes an error
866 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
867 .{ .slice = "\"a\xF0\x90\x80\x80\"", .code_page = .utf8 },
868 false,
869 .{},
870 ));
871 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
872 .{ .slice = "\"\xF0\x90\x80\x80a\"", .code_page = .utf8 },
873 false,
874 .{},
875 ));
876}
877
878pub const ForcedOrdinal = struct {
879 pub fn fromBytes(bytes: SourceBytes) u16 {
880 var i: usize = 0;
881 var result: u21 = 0;
882 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
883 const c = switch (codepoint.value) {
884 // Codepoints that would need a surrogate pair in UTF-16 are
885 // broken up into their UTF-16 code units and each code unit
886 // is interpreted as a digit.
887 0x10000...0x10FFFF => {
888 const high = @as(u16, @intCast((codepoint.value - 0x10000) >> 10)) + 0xD800;
889 if (result != 0) result *%= 10;
890 result +%= high -% '0';
891
892 const low = @as(u16, @intCast(codepoint.value & 0x3FF)) + 0xDC00;
893 if (result != 0) result *%= 10;
894 result +%= low -% '0';
895 continue;
896 },
897 Codepoint.invalid => 0xFFFD,
898 else => codepoint.value,
899 };
900 if (result != 0) result *%= 10;
901 result +%= c -% '0';
902 }
903 return @truncate(result);
904 }
905
906 pub fn fromUtf16Le(utf16: [:0]const u16) u16 {
907 var result: u16 = 0;
908 for (utf16) |code_unit| {
909 if (result != 0) result *%= 10;
910 result +%= std.mem.littleToNative(u16, code_unit) -% '0';
911 }
912 return result;
913 }
914};
915
916test "forced ordinal" {
917 try std.testing.expectEqual(@as(u16, 3200), ForcedOrdinal.fromBytes(.{ .slice = "3200", .code_page = .windows1252 }));
918 try std.testing.expectEqual(@as(u16, 0x33), ForcedOrdinal.fromBytes(.{ .slice = "1+1", .code_page = .windows1252 }));
919 try std.testing.expectEqual(@as(u16, 65531), ForcedOrdinal.fromBytes(.{ .slice = "1!", .code_page = .windows1252 }));
920
921 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0\x8C", .code_page = .windows1252 }));
922 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0Œ", .code_page = .utf8 }));
923
924 // invalid UTF-8 gets converted to 0xFFFD (replacement char) and then interpreted as a digit
925 try std.testing.expectEqual(@as(u16, 0xFFCD), ForcedOrdinal.fromBytes(.{ .slice = "0\x81", .code_page = .utf8 }));
926 // codepoints >= 0x10000
927 try std.testing.expectEqual(@as(u16, 0x49F2), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10002}", .code_page = .utf8 }));
928 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10100}", .code_page = .utf8 }));
929
930 // From UTF-16
931 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromUtf16Le(&[_:0]u16{ std.mem.nativeToLittle(u16, '0'), std.mem.nativeToLittle(u16, 'Œ') }));
932 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromUtf16Le(std.unicode.utf8ToUtf16LeStringLiteral("0\u{10100}")));
933}
934
935/// https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
936pub const FixedFileInfo = struct {
937 file_version: Version = .{},
938 product_version: Version = .{},
939 file_flags_mask: u32 = 0,
940 file_flags: u32 = 0,
941 file_os: u32 = 0,
942 file_type: u32 = 0,
943 file_subtype: u32 = 0,
944 file_date: Version = .{}, // TODO: I think this is always all zeroes?
945
946 pub const signature = 0xFEEF04BD;
947 // Note: This corresponds to a version of 1.0
948 pub const version = 0x00010000;
949
950 pub const byte_len = 0x34;
951 pub const key = std.unicode.utf8ToUtf16LeStringLiteral("VS_VERSION_INFO");
952
953 pub const Version = struct {
954 parts: [4]u16 = [_]u16{0} ** 4,
955
956 pub fn mostSignificantCombinedParts(self: Version) u32 {
957 return (@as(u32, self.parts[0]) << 16) + self.parts[1];
958 }
959
960 pub fn leastSignificantCombinedParts(self: Version) u32 {
961 return (@as(u32, self.parts[2]) << 16) + self.parts[3];
962 }
963 };
964
965 pub fn write(self: FixedFileInfo, writer: anytype) !void {
966 try writer.writeInt(u32, signature, .little);
967 try writer.writeInt(u32, version, .little);
968 try writer.writeInt(u32, self.file_version.mostSignificantCombinedParts(), .little);
969 try writer.writeInt(u32, self.file_version.leastSignificantCombinedParts(), .little);
970 try writer.writeInt(u32, self.product_version.mostSignificantCombinedParts(), .little);
971 try writer.writeInt(u32, self.product_version.leastSignificantCombinedParts(), .little);
972 try writer.writeInt(u32, self.file_flags_mask, .little);
973 try writer.writeInt(u32, self.file_flags, .little);
974 try writer.writeInt(u32, self.file_os, .little);
975 try writer.writeInt(u32, self.file_type, .little);
976 try writer.writeInt(u32, self.file_subtype, .little);
977 try writer.writeInt(u32, self.file_date.mostSignificantCombinedParts(), .little);
978 try writer.writeInt(u32, self.file_date.leastSignificantCombinedParts(), .little);
979 }
980};
981
982test "FixedFileInfo.Version" {
983 const version = FixedFileInfo.Version{
984 .parts = .{ 1, 2, 3, 4 },
985 };
986 try std.testing.expectEqual(@as(u32, 0x00010002), version.mostSignificantCombinedParts());
987 try std.testing.expectEqual(@as(u32, 0x00030004), version.leastSignificantCombinedParts());
988}
989
990pub const VersionNode = struct {
991 pub const type_string: u16 = 1;
992 pub const type_binary: u16 = 0;
993};
994
995pub const MenuItemFlags = struct {
996 value: u16 = 0,
997
998 pub fn apply(self: *MenuItemFlags, option: rc.MenuItem.Option) void {
999 self.value |= optionValue(option);
1000 }
1001
1002 pub fn isSet(self: MenuItemFlags, option: rc.MenuItem.Option) bool {
1003 return self.value & optionValue(option) != 0;
1004 }
1005
1006 fn optionValue(option: rc.MenuItem.Option) u16 {
1007 return @intCast(switch (option) {
1008 .checked => MF.CHECKED,
1009 .grayed => MF.GRAYED,
1010 .help => MF.HELP,
1011 .inactive => MF.DISABLED,
1012 .menubarbreak => MF.MENUBARBREAK,
1013 .menubreak => MF.MENUBREAK,
1014 });
1015 }
1016
1017 pub fn markLast(self: *MenuItemFlags) void {
1018 self.value |= @intCast(MF.END);
1019 }
1020};
1021
1022/// Menu Flags from WinUser.h
1023/// This is not complete, it only contains what is needed
1024pub const MF = struct {
1025 pub const GRAYED: u32 = 0x00000001;
1026 pub const DISABLED: u32 = 0x00000002;
1027 pub const CHECKED: u32 = 0x00000008;
1028 pub const POPUP: u32 = 0x00000010;
1029 pub const MENUBARBREAK: u32 = 0x00000020;
1030 pub const MENUBREAK: u32 = 0x00000040;
1031 pub const HELP: u32 = 0x00004000;
1032 pub const END: u32 = 0x00000080;
1033};
1034
1035/// Window Styles from WinUser.h
1036pub const WS = struct {
1037 pub const OVERLAPPED: u32 = 0x00000000;
1038 pub const POPUP: u32 = 0x80000000;
1039 pub const CHILD: u32 = 0x40000000;
1040 pub const MINIMIZE: u32 = 0x20000000;
1041 pub const VISIBLE: u32 = 0x10000000;
1042 pub const DISABLED: u32 = 0x08000000;
1043 pub const CLIPSIBLINGS: u32 = 0x04000000;
1044 pub const CLIPCHILDREN: u32 = 0x02000000;
1045 pub const MAXIMIZE: u32 = 0x01000000;
1046 pub const CAPTION: u32 = BORDER | DLGFRAME;
1047 pub const BORDER: u32 = 0x00800000;
1048 pub const DLGFRAME: u32 = 0x00400000;
1049 pub const VSCROLL: u32 = 0x00200000;
1050 pub const HSCROLL: u32 = 0x00100000;
1051 pub const SYSMENU: u32 = 0x00080000;
1052 pub const THICKFRAME: u32 = 0x00040000;
1053 pub const GROUP: u32 = 0x00020000;
1054 pub const TABSTOP: u32 = 0x00010000;
1055
1056 pub const MINIMIZEBOX: u32 = 0x00020000;
1057 pub const MAXIMIZEBOX: u32 = 0x00010000;
1058
1059 pub const TILED: u32 = OVERLAPPED;
1060 pub const ICONIC: u32 = MINIMIZE;
1061 pub const SIZEBOX: u32 = THICKFRAME;
1062 pub const TILEDWINDOW: u32 = OVERLAPPEDWINDOW;
1063
1064 // Common Window Styles
1065 pub const OVERLAPPEDWINDOW: u32 = OVERLAPPED | CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX;
1066 pub const POPUPWINDOW: u32 = POPUP | BORDER | SYSMENU;
1067 pub const CHILDWINDOW: u32 = CHILD;
1068};
1069
1070/// Dialog Box Template Styles from WinUser.h
1071pub const DS = struct {
1072 pub const SETFONT: u32 = 0x40;
1073};
1074
1075/// Button Control Styles from WinUser.h
1076/// This is not complete, it only contains what is needed
1077pub const BS = struct {
1078 pub const PUSHBUTTON: u32 = 0x00000000;
1079 pub const DEFPUSHBUTTON: u32 = 0x00000001;
1080 pub const CHECKBOX: u32 = 0x00000002;
1081 pub const AUTOCHECKBOX: u32 = 0x00000003;
1082 pub const RADIOBUTTON: u32 = 0x00000004;
1083 pub const @"3STATE": u32 = 0x00000005;
1084 pub const AUTO3STATE: u32 = 0x00000006;
1085 pub const GROUPBOX: u32 = 0x00000007;
1086 pub const USERBUTTON: u32 = 0x00000008;
1087 pub const AUTORADIOBUTTON: u32 = 0x00000009;
1088 pub const PUSHBOX: u32 = 0x0000000A;
1089 pub const OWNERDRAW: u32 = 0x0000000B;
1090 pub const TYPEMASK: u32 = 0x0000000F;
1091 pub const LEFTTEXT: u32 = 0x00000020;
1092};
1093
1094/// Static Control Constants from WinUser.h
1095/// This is not complete, it only contains what is needed
1096pub const SS = struct {
1097 pub const LEFT: u32 = 0x00000000;
1098 pub const CENTER: u32 = 0x00000001;
1099 pub const RIGHT: u32 = 0x00000002;
1100 pub const ICON: u32 = 0x00000003;
1101};
1102
1103/// Listbox Styles from WinUser.h
1104/// This is not complete, it only contains what is needed
1105pub const LBS = struct {
1106 pub const NOTIFY: u32 = 0x0001;
1107};
lib/compiler/resinator/source_mapping.zig created+831
...@@ -0,0 +1,831 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const utils = @import("utils.zig");
4const UncheckedSliceWriter = utils.UncheckedSliceWriter;
5
6pub const ParseLineCommandsResult = struct {
7 result: []u8,
8 mappings: SourceMappings,
9};
10
11const CurrentMapping = struct {
12 line_num: usize = 1,
13 filename: std.ArrayListUnmanaged(u8) = .{},
14 pending: bool = true,
15 ignore_contents: bool = false,
16};
17
18pub const ParseAndRemoveLineCommandsOptions = struct {
19 initial_filename: ?[]const u8 = null,
20};
21
22/// Parses and removes #line commands as well as all source code that is within a file
23/// with .c or .h extensions.
24///
25/// > RC treats files with the .c and .h extensions in a special manner. It
26/// > assumes that a file with one of these extensions does not contain
27/// > resources. If a file has the .c or .h file name extension, RC ignores all
28/// > lines in the file except the preprocessor directives. Therefore, to
29/// > include a file that contains resources in another resource script, give
30/// > the file to be included an extension other than .c or .h.
31/// from https://learn.microsoft.com/en-us/windows/win32/menurc/preprocessor-directives
32///
33/// Returns a slice of `buf` with the aforementioned stuff removed as well as a mapping
34/// between the lines and their corresponding lines in their original files.
35///
36/// `buf` must be at least as long as `source`
37/// In-place transformation is supported (i.e. `source` and `buf` can be the same slice)
38///
39/// If `options.initial_filename` is provided, that filename is guaranteed to be
40/// within the `mappings.files` table and `root_filename_offset` will be set appropriately.
41pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult {
42 var parse_result = ParseLineCommandsResult{
43 .result = undefined,
44 .mappings = .{},
45 };
46 errdefer parse_result.mappings.deinit(allocator);
47
48 var current_mapping: CurrentMapping = .{};
49 defer current_mapping.filename.deinit(allocator);
50
51 if (options.initial_filename) |initial_filename| {
52 try current_mapping.filename.appendSlice(allocator, initial_filename);
53 parse_result.mappings.root_filename_offset = try parse_result.mappings.files.put(allocator, initial_filename);
54 }
55
56 std.debug.assert(buf.len >= source.len);
57 var result = UncheckedSliceWriter{ .slice = buf };
58 const State = enum {
59 line_start,
60 preprocessor,
61 non_preprocessor,
62 };
63 var state: State = .line_start;
64 var index: usize = 0;
65 var pending_start: ?usize = null;
66 var preprocessor_start: usize = 0;
67 var line_number: usize = 1;
68 while (index < source.len) : (index += 1) {
69 const c = source[index];
70 switch (state) {
71 .line_start => switch (c) {
72 '#' => {
73 preprocessor_start = index;
74 state = .preprocessor;
75 if (pending_start == null) {
76 pending_start = index;
77 }
78 },
79 '\r', '\n' => {
80 const is_crlf = formsLineEndingPair(source, c, index + 1);
81 if (!current_mapping.ignore_contents) {
82 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
83
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 if (!current_mapping.ignore_contents) {
119 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
120
121 const line_ending_len: usize = if (is_crlf) 2 else 1;
122 result.writeSlice(source[pending_start.? .. index + line_ending_len]);
123 line_number += 1;
124 }
125 }
126 if (is_crlf) index += 1;
127 state = .line_start;
128 pending_start = null;
129 },
130 else => {},
131 },
132 .non_preprocessor => switch (c) {
133 '\r', '\n' => {
134 const is_crlf = formsLineEndingPair(source, c, index + 1);
135 if (!current_mapping.ignore_contents) {
136 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
137
138 result.write(c);
139 if (is_crlf) result.write(source[index + 1]);
140 line_number += 1;
141 }
142 if (is_crlf) index += 1;
143 state = .line_start;
144 pending_start = null;
145 },
146 else => {
147 if (!current_mapping.ignore_contents) {
148 result.write(c);
149 }
150 },
151 },
152 }
153 } else {
154 switch (state) {
155 .line_start => {},
156 .non_preprocessor => {
157 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
158 },
159 .preprocessor => {
160 // Now that we have the full line we can decide what to do with it
161 const preprocessor_str = source[preprocessor_start..index];
162 if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
163 try handleLineCommand(allocator, preprocessor_str, &current_mapping);
164 } else {
165 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
166 if (!current_mapping.ignore_contents) {
167 result.writeSlice(source[pending_start.?..index]);
168 }
169 }
170 },
171 }
172 }
173
174 parse_result.result = result.getWritten();
175
176 // Remove whitespace from the end of the result. This avoids issues when the
177 // preprocessor adds a newline to the end of the file, since then the
178 // post-preprocessed source could have more lines than the corresponding input source and
179 // the inserted line can't be mapped to any lines in the original file.
180 // There's no way that whitespace at the end of a file can affect the parsing
181 // of the RC script so this is okay to do unconditionally.
182 // TODO: There might be a better way around this
183 while (parse_result.result.len > 0 and std.ascii.isWhitespace(parse_result.result[parse_result.result.len - 1])) {
184 parse_result.result.len -= 1;
185 }
186
187 // If there have been no line mappings at all, then we're dealing with an empty file.
188 // In this case, we want to fake a line mapping just so that we return something
189 // that is useable in the same way that a non-empty mapping would be.
190 if (parse_result.mappings.sources.root == null) {
191 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
192 }
193
194 return parse_result;
195}
196
197/// Note: This should function the same as lex.LineHandler.currentIndexFormsLineEndingPair
198pub fn formsLineEndingPair(source: []const u8, line_ending: u8, next_index: usize) bool {
199 if (next_index >= source.len) return false;
200
201 const next_ending = source[next_index];
202 return utils.isLineEndingPair(line_ending, next_ending);
203}
204
205pub fn handleLineEnd(allocator: Allocator, post_processed_line_number: usize, mapping: *SourceMappings, current_mapping: *CurrentMapping) !void {
206 const filename_offset = try mapping.files.put(allocator, current_mapping.filename.items);
207
208 try mapping.set(post_processed_line_number, current_mapping.line_num, filename_offset);
209
210 current_mapping.line_num += 1;
211 current_mapping.pending = false;
212}
213
214// TODO: Might want to provide diagnostics on invalid line commands instead of just returning
215pub fn handleLineCommand(allocator: Allocator, line_command: []const u8, current_mapping: *CurrentMapping) error{OutOfMemory}!void {
216 // TODO: Are there other whitespace characters that should be included?
217 var tokenizer = std.mem.tokenize(u8, line_command, " \t");
218 const line_directive = tokenizer.next() orelse return; // #line
219 if (!std.mem.eql(u8, line_directive, "#line")) return;
220 const linenum_str = tokenizer.next() orelse return;
221 const linenum = std.fmt.parseUnsigned(usize, linenum_str, 10) catch return;
222
223 var filename_literal = tokenizer.rest();
224 while (filename_literal.len > 0 and std.ascii.isWhitespace(filename_literal[filename_literal.len - 1])) {
225 filename_literal.len -= 1;
226 }
227 if (filename_literal.len < 2) return;
228 const is_quoted = filename_literal[0] == '"' and filename_literal[filename_literal.len - 1] == '"';
229 if (!is_quoted) return;
230 const filename = parseFilename(allocator, filename_literal[1 .. filename_literal.len - 1]) catch |err| switch (err) {
231 error.OutOfMemory => |e| return e,
232 else => return,
233 };
234 defer allocator.free(filename);
235
236 // \x00 bytes in the filename is incompatible with how StringTable works
237 if (std.mem.indexOfScalar(u8, filename, '\x00') != null) return;
238
239 current_mapping.line_num = linenum;
240 current_mapping.filename.clearRetainingCapacity();
241 try current_mapping.filename.appendSlice(allocator, filename);
242 current_mapping.pending = true;
243 current_mapping.ignore_contents = std.ascii.endsWithIgnoreCase(filename, ".c") or std.ascii.endsWithIgnoreCase(filename, ".h");
244}
245
246pub fn parseAndRemoveLineCommandsAlloc(allocator: Allocator, source: []const u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult {
247 const buf = try allocator.alloc(u8, source.len);
248 errdefer allocator.free(buf);
249 var result = try parseAndRemoveLineCommands(allocator, source, buf, options);
250 result.result = try allocator.realloc(buf, result.result.len);
251 return result;
252}
253
254/// C-style string parsing with a few caveats:
255/// - The str cannot contain newlines or carriage returns
256/// - Hex and octal escape are limited to u8
257/// - No handling/support for L, u, or U prefixed strings
258/// - The start and end double quotes should be omitted from the `str`
259/// - Other than the above, does not assume any validity of the strings (i.e. there
260/// may be unescaped double quotes within the str) and will return error.InvalidString
261/// on any problems found.
262///
263/// The result is a UTF-8 encoded string.
264fn parseFilename(allocator: Allocator, str: []const u8) error{ OutOfMemory, InvalidString }![]u8 {
265 const State = enum {
266 string,
267 escape,
268 escape_hex,
269 escape_octal,
270 escape_u,
271 };
272
273 var filename = try std.ArrayList(u8).initCapacity(allocator, str.len);
274 errdefer filename.deinit();
275 var state: State = .string;
276 var index: usize = 0;
277 var escape_len: usize = undefined;
278 var escape_val: u64 = undefined;
279 var escape_expected_len: u8 = undefined;
280 while (index < str.len) : (index += 1) {
281 const c = str[index];
282 switch (state) {
283 .string => switch (c) {
284 '\\' => state = .escape,
285 '"' => return error.InvalidString,
286 else => filename.appendAssumeCapacity(c),
287 },
288 .escape => switch (c) {
289 '\'', '"', '\\', '?', 'n', 'r', 't', 'a', 'b', 'e', 'f', 'v' => {
290 const escaped_c = switch (c) {
291 '\'', '"', '\\', '?' => c,
292 'n' => '\n',
293 'r' => '\r',
294 't' => '\t',
295 'a' => '\x07',
296 'b' => '\x08',
297 'e' => '\x1b', // non-standard
298 'f' => '\x0c',
299 'v' => '\x0b',
300 else => unreachable,
301 };
302 filename.appendAssumeCapacity(escaped_c);
303 state = .string;
304 },
305 'x' => {
306 escape_val = 0;
307 escape_len = 0;
308 state = .escape_hex;
309 },
310 '0'...'7' => {
311 escape_val = std.fmt.charToDigit(c, 8) catch unreachable;
312 escape_len = 1;
313 state = .escape_octal;
314 },
315 'u' => {
316 escape_val = 0;
317 escape_len = 0;
318 state = .escape_u;
319 escape_expected_len = 4;
320 },
321 'U' => {
322 escape_val = 0;
323 escape_len = 0;
324 state = .escape_u;
325 escape_expected_len = 8;
326 },
327 else => return error.InvalidString,
328 },
329 .escape_hex => switch (c) {
330 '0'...'9', 'a'...'f', 'A'...'F' => {
331 const digit = std.fmt.charToDigit(c, 16) catch unreachable;
332 if (escape_val != 0) escape_val = std.math.mul(u8, @as(u8, @intCast(escape_val)), 16) catch return error.InvalidString;
333 escape_val = std.math.add(u8, @as(u8, @intCast(escape_val)), digit) catch return error.InvalidString;
334 escape_len += 1;
335 },
336 else => {
337 if (escape_len == 0) return error.InvalidString;
338 filename.appendAssumeCapacity(@intCast(escape_val));
339 state = .string;
340 index -= 1; // reconsume
341 },
342 },
343 .escape_octal => switch (c) {
344 '0'...'7' => {
345 const digit = std.fmt.charToDigit(c, 8) catch unreachable;
346 if (escape_val != 0) escape_val = std.math.mul(u8, @as(u8, @intCast(escape_val)), 8) catch return error.InvalidString;
347 escape_val = std.math.add(u8, @as(u8, @intCast(escape_val)), digit) catch return error.InvalidString;
348 escape_len += 1;
349 if (escape_len == 3) {
350 filename.appendAssumeCapacity(@intCast(escape_val));
351 state = .string;
352 }
353 },
354 else => {
355 if (escape_len == 0) return error.InvalidString;
356 filename.appendAssumeCapacity(@intCast(escape_val));
357 state = .string;
358 index -= 1; // reconsume
359 },
360 },
361 .escape_u => switch (c) {
362 '0'...'9', 'a'...'f', 'A'...'F' => {
363 const digit = std.fmt.charToDigit(c, 16) catch unreachable;
364 if (escape_val != 0) escape_val = std.math.mul(u21, @as(u21, @intCast(escape_val)), 16) catch return error.InvalidString;
365 escape_val = std.math.add(u21, @as(u21, @intCast(escape_val)), digit) catch return error.InvalidString;
366 escape_len += 1;
367 if (escape_len == escape_expected_len) {
368 var buf: [4]u8 = undefined;
369 const utf8_len = std.unicode.utf8Encode(@intCast(escape_val), &buf) catch return error.InvalidString;
370 filename.appendSliceAssumeCapacity(buf[0..utf8_len]);
371 state = .string;
372 }
373 },
374 // Requires escape_expected_len valid hex digits
375 else => return error.InvalidString,
376 },
377 }
378 } else {
379 switch (state) {
380 .string => {},
381 .escape, .escape_u => return error.InvalidString,
382 .escape_hex => {
383 if (escape_len == 0) return error.InvalidString;
384 filename.appendAssumeCapacity(@intCast(escape_val));
385 },
386 .escape_octal => {
387 filename.appendAssumeCapacity(@intCast(escape_val));
388 },
389 }
390 }
391
392 return filename.toOwnedSlice();
393}
394
395fn testParseFilename(expected: []const u8, input: []const u8) !void {
396 const parsed = try parseFilename(std.testing.allocator, input);
397 defer std.testing.allocator.free(parsed);
398
399 return std.testing.expectEqualSlices(u8, expected, parsed);
400}
401
402test parseFilename {
403 try testParseFilename("'\"?\\\t\n\r\x11", "\\'\\\"\\?\\\\\\t\\n\\r\\x11");
404 try testParseFilename("\xABz\x53", "\\xABz\\123");
405 try testParseFilename("⚡⚡", "\\u26A1\\U000026A1");
406 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\""));
407 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\"));
408 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\u"));
409 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\U"));
410 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\x"));
411 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\xZZ"));
412 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\xABCDEF"));
413 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\777"));
414}
415
416pub const SourceMappings = struct {
417 sources: Sources = .{},
418 files: StringTable = .{},
419 /// The default assumes that the first filename added is the root file.
420 /// The value should be set to the correct offset if that assumption does not hold.
421 root_filename_offset: u32 = 0,
422 source_node_pool: std.heap.MemoryPool(Sources.Node) = std.heap.MemoryPool(Sources.Node).init(std.heap.page_allocator),
423 end_line: usize = 0,
424
425 const sourceCompare = struct {
426 fn compare(a: Source, b: Source) std.math.Order {
427 return std.math.order(a.start_line, b.start_line);
428 }
429 }.compare;
430 const Sources = std.Treap(Source, sourceCompare);
431
432 pub const Source = struct {
433 start_line: usize,
434 span: usize = 0,
435 corresponding_start_line: usize,
436 filename_offset: u32,
437 };
438
439 pub fn deinit(self: *SourceMappings, allocator: Allocator) void {
440 self.files.deinit(allocator);
441 self.source_node_pool.deinit();
442 }
443
444 /// Find the node that 'contains' the `line`, i.e. the node's start_line is
445 /// >= `line`
446 fn findNode(self: SourceMappings, line: usize) ?*Sources.Node {
447 var node = self.sources.root;
448 var last_gt: ?*Sources.Node = null;
449
450 var search_key: Source = undefined;
451 search_key.start_line = line;
452 while (node) |current| {
453 const order = sourceCompare(search_key, current.key);
454 if (order == .eq) break;
455 if (order == .gt) last_gt = current;
456
457 node = current.children[@intFromBool(order == .gt)] orelse {
458 // Regardless of the current order, last_gt will contain the
459 // the node we want to return.
460 //
461 // If search key is > current node's key, then last_gt will be
462 // current which we now know is the closest node that is <=
463 // the search key.
464 //
465 //
466 // If the key is < current node's key, we want to jump back to the
467 // node that the search key was most recently greater than.
468 // This is necessary for scenarios like (where the search key is 2):
469 //
470 // 1
471 // \
472 // 6
473 // /
474 // 3
475 //
476 // In this example, we'll get down to the '3' node but ultimately want
477 // to return the '1' node.
478 //
479 // Note: If we've never seen a key that the search key is greater than,
480 // then we know that there's no valid node, so last_gt will be null.
481 return last_gt;
482 };
483 }
484
485 return node;
486 }
487
488 /// Note: `line_num` and `corresponding_line_num` start at 1
489 pub fn set(self: *SourceMappings, line_num: usize, corresponding_line_num: usize, filename_offset: u32) !void {
490 const maybe_node = self.findNode(line_num);
491
492 const need_new_node = need_new_node: {
493 if (maybe_node) |node| {
494 if (node.key.filename_offset != filename_offset) {
495 break :need_new_node true;
496 }
497 const exist_delta = @as(i64, @intCast(node.key.corresponding_start_line)) - @as(i64, @intCast(node.key.start_line));
498 const cur_delta = @as(i64, @intCast(corresponding_line_num)) - @as(i64, @intCast(line_num));
499 if (exist_delta != cur_delta) {
500 break :need_new_node true;
501 }
502 break :need_new_node false;
503 }
504 break :need_new_node true;
505 };
506 if (need_new_node) {
507 // spans must not overlap
508 if (maybe_node) |node| {
509 std.debug.assert(node.key.start_line != line_num);
510 }
511
512 const key = Source{
513 .start_line = line_num,
514 .corresponding_start_line = corresponding_line_num,
515 .filename_offset = filename_offset,
516 };
517 var entry = self.sources.getEntryFor(key);
518 var new_node = try self.source_node_pool.create();
519 new_node.key = key;
520 entry.set(new_node);
521 }
522 if (line_num > self.end_line) {
523 self.end_line = line_num;
524 }
525 }
526
527 /// Note: `line_num` starts at 1
528 pub fn get(self: SourceMappings, line_num: usize) ?Source {
529 const node = self.findNode(line_num) orelse return null;
530 return node.key;
531 }
532
533 pub const CorrespondingSpan = struct {
534 start_line: usize,
535 end_line: usize,
536 filename_offset: u32,
537 };
538
539 pub fn getCorrespondingSpan(self: SourceMappings, line_num: usize) ?CorrespondingSpan {
540 const source = self.get(line_num) orelse return null;
541 const diff = line_num - source.start_line;
542 const start_line = source.corresponding_start_line + (if (line_num == source.start_line) 0 else source.span + diff);
543 const end_line = start_line + (if (line_num == source.start_line) source.span else 0);
544 return CorrespondingSpan{
545 .start_line = start_line,
546 .end_line = end_line,
547 .filename_offset = source.filename_offset,
548 };
549 }
550
551 pub fn collapse(self: *SourceMappings, line_num: usize, num_following_lines_to_collapse: usize) !void {
552 std.debug.assert(num_following_lines_to_collapse > 0);
553 var node = self.findNode(line_num).?;
554 const span_diff = num_following_lines_to_collapse;
555 if (node.key.start_line != line_num) {
556 const offset = line_num - node.key.start_line;
557 const key = Source{
558 .start_line = line_num,
559 .span = num_following_lines_to_collapse,
560 .corresponding_start_line = node.key.corresponding_start_line + node.key.span + offset,
561 .filename_offset = node.key.filename_offset,
562 };
563 var entry = self.sources.getEntryFor(key);
564 var new_node = try self.source_node_pool.create();
565 new_node.key = key;
566 entry.set(new_node);
567 node = new_node;
568 } else {
569 node.key.span += span_diff;
570 }
571
572 // now subtract the span diff from the start line number of all of
573 // the following nodes in order
574 var it = Sources.InorderIterator{
575 .current = node,
576 .previous = node.children[0],
577 };
578 // skip past current, but store it
579 var prev = it.next().?;
580 while (it.next()) |inorder_node| {
581 inorder_node.key.start_line -= span_diff;
582
583 // This can only really happen if there are #line commands within
584 // a multiline comment, which in theory should be skipped over.
585 // However, currently, parseAndRemoveLineCommands is not aware of
586 // comments at all.
587 //
588 // TODO: Make parseAndRemoveLineCommands aware of comments/strings
589 // and turn this into an assertion
590 if (prev.key.start_line > inorder_node.key.start_line) {
591 return error.InvalidSourceMappingCollapse;
592 }
593 prev = inorder_node;
594 }
595 self.end_line -= span_diff;
596 }
597
598 /// Returns true if the line is from the main/root file (i.e. not a file that has been
599 /// `#include`d).
600 pub fn isRootFile(self: *SourceMappings, line_num: usize) bool {
601 const source = self.get(line_num) orelse return false;
602 return source.filename_offset == self.root_filename_offset;
603 }
604};
605
606test "SourceMappings collapse" {
607 const allocator = std.testing.allocator;
608
609 var mappings = SourceMappings{};
610 defer mappings.deinit(allocator);
611 const filename_offset = try mappings.files.put(allocator, "test.rc");
612
613 try mappings.set(1, 1, filename_offset);
614 try mappings.set(5, 5, filename_offset);
615
616 try mappings.collapse(2, 2);
617
618 try std.testing.expectEqual(@as(usize, 3), mappings.end_line);
619 const span_1 = mappings.getCorrespondingSpan(1).?;
620 try std.testing.expectEqual(@as(usize, 1), span_1.start_line);
621 try std.testing.expectEqual(@as(usize, 1), span_1.end_line);
622 const span_2 = mappings.getCorrespondingSpan(2).?;
623 try std.testing.expectEqual(@as(usize, 2), span_2.start_line);
624 try std.testing.expectEqual(@as(usize, 4), span_2.end_line);
625 const span_3 = mappings.getCorrespondingSpan(3).?;
626 try std.testing.expectEqual(@as(usize, 5), span_3.start_line);
627 try std.testing.expectEqual(@as(usize, 5), span_3.end_line);
628}
629
630/// Same thing as StringTable in Zig's src/Wasm.zig
631pub const StringTable = struct {
632 data: std.ArrayListUnmanaged(u8) = .{},
633 map: std.HashMapUnmanaged(u32, void, std.hash_map.StringIndexContext, std.hash_map.default_max_load_percentage) = .{},
634
635 pub fn deinit(self: *StringTable, allocator: Allocator) void {
636 self.data.deinit(allocator);
637 self.map.deinit(allocator);
638 }
639
640 pub fn put(self: *StringTable, allocator: Allocator, value: []const u8) !u32 {
641 const result = try self.map.getOrPutContextAdapted(
642 allocator,
643 value,
644 std.hash_map.StringIndexAdapter{ .bytes = &self.data },
645 .{ .bytes = &self.data },
646 );
647 if (result.found_existing) {
648 return result.key_ptr.*;
649 }
650
651 try self.data.ensureUnusedCapacity(allocator, value.len + 1);
652 const offset: u32 = @intCast(self.data.items.len);
653
654 self.data.appendSliceAssumeCapacity(value);
655 self.data.appendAssumeCapacity(0);
656
657 result.key_ptr.* = offset;
658
659 return offset;
660 }
661
662 pub fn get(self: StringTable, offset: u32) []const u8 {
663 std.debug.assert(offset < self.data.items.len);
664 return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(self.data.items.ptr + offset)), 0);
665 }
666
667 pub fn getOffset(self: *StringTable, value: []const u8) ?u32 {
668 return self.map.getKeyAdapted(
669 value,
670 std.hash_map.StringIndexAdapter{ .bytes = &self.data },
671 );
672 }
673};
674
675const ExpectedSourceSpan = struct {
676 start_line: usize,
677 end_line: usize,
678 filename: []const u8,
679};
680
681fn testParseAndRemoveLineCommands(
682 expected: []const u8,
683 comptime expected_spans: []const ExpectedSourceSpan,
684 source: []const u8,
685 options: ParseAndRemoveLineCommandsOptions,
686) !void {
687 var results = try parseAndRemoveLineCommandsAlloc(std.testing.allocator, source, options);
688 defer std.testing.allocator.free(results.result);
689 defer results.mappings.deinit(std.testing.allocator);
690
691 try std.testing.expectEqualStrings(expected, results.result);
692
693 expectEqualMappings(expected_spans, results.mappings) catch |err| {
694 std.debug.print("\nexpected mappings:\n", .{});
695 for (expected_spans, 0..) |span, i| {
696 const line_num = i + 1;
697 std.debug.print("{}: {s}:{}-{}\n", .{ line_num, span.filename, span.start_line, span.end_line });
698 }
699 std.debug.print("\nactual mappings:\n", .{});
700 var i: usize = 1;
701 while (i <= results.mappings.end_line) : (i += 1) {
702 const span = results.mappings.getCorrespondingSpan(i).?;
703 const filename = results.mappings.files.get(span.filename_offset);
704 std.debug.print("{}: {s}:{}-{}\n", .{ i, filename, span.start_line, span.end_line });
705 }
706 std.debug.print("\n", .{});
707 return err;
708 };
709}
710
711fn expectEqualMappings(expected_spans: []const ExpectedSourceSpan, mappings: SourceMappings) !void {
712 try std.testing.expectEqual(expected_spans.len, mappings.end_line);
713 for (expected_spans, 0..) |expected_span, i| {
714 const line_num = i + 1;
715 const span = mappings.getCorrespondingSpan(line_num) orelse return error.MissingLineNum;
716 const filename = mappings.files.get(span.filename_offset);
717 try std.testing.expectEqual(expected_span.start_line, span.start_line);
718 try std.testing.expectEqual(expected_span.end_line, span.end_line);
719 try std.testing.expectEqualStrings(expected_span.filename, filename);
720 }
721}
722
723test "basic" {
724 try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{
725 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
726 }, "#line 1 \"blah.rc\"", .{});
727}
728
729test "only removes line commands" {
730 try testParseAndRemoveLineCommands(
731 \\#pragma code_page(65001)
732 , &[_]ExpectedSourceSpan{
733 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
734 },
735 \\#line 1 "blah.rc"
736 \\#pragma code_page(65001)
737 , .{});
738}
739
740test "whitespace and line endings" {
741 try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{
742 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
743 }, "#line \t 1 \t \"blah.rc\"\r\n", .{});
744}
745
746test "example" {
747 try testParseAndRemoveLineCommands(
748 \\
749 \\included RCDATA {"hello"}
750 , &[_]ExpectedSourceSpan{
751 .{ .start_line = 1, .end_line = 1, .filename = "./included.rc" },
752 .{ .start_line = 2, .end_line = 2, .filename = "./included.rc" },
753 },
754 \\#line 1 "rcdata.rc"
755 \\#line 1 "<built-in>"
756 \\#line 1 "<built-in>"
757 \\#line 355 "<built-in>"
758 \\#line 1 "<command line>"
759 \\#line 1 "<built-in>"
760 \\#line 1 "rcdata.rc"
761 \\#line 1 "./header.h"
762 \\
763 \\
764 \\2 RCDATA {"blah"}
765 \\
766 \\
767 \\#line 1 "./included.rc"
768 \\
769 \\included RCDATA {"hello"}
770 \\#line 7 "./header.h"
771 \\#line 1 "rcdata.rc"
772 , .{});
773}
774
775test "CRLF and other line endings" {
776 try testParseAndRemoveLineCommands(
777 "hello\r\n#pragma code_page(65001)\r\nworld",
778 &[_]ExpectedSourceSpan{
779 .{ .start_line = 1, .end_line = 1, .filename = "crlf.rc" },
780 .{ .start_line = 2, .end_line = 2, .filename = "crlf.rc" },
781 .{ .start_line = 3, .end_line = 3, .filename = "crlf.rc" },
782 },
783 "#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",
784 .{},
785 );
786}
787
788test "no line commands" {
789 try testParseAndRemoveLineCommands(
790 \\1 RCDATA {"blah"}
791 \\2 RCDATA {"blah"}
792 , &[_]ExpectedSourceSpan{
793 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
794 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
795 },
796 \\1 RCDATA {"blah"}
797 \\2 RCDATA {"blah"}
798 , .{ .initial_filename = "blah.rc" });
799}
800
801test "in place" {
802 var mut_source = "#line 1 \"blah.rc\"".*;
803 var result = try parseAndRemoveLineCommands(std.testing.allocator, &mut_source, &mut_source, .{});
804 defer result.mappings.deinit(std.testing.allocator);
805 try std.testing.expectEqualStrings("", result.result);
806}
807
808test "line command within a multiline comment" {
809 // TODO: Enable once parseAndRemoveLineCommands is comment-aware
810 if (true) return error.SkipZigTest;
811
812 try testParseAndRemoveLineCommands(
813 \\/*
814 \\#line 1 "irrelevant.rc"
815 \\
816 \\
817 \\*/
818 , &[_]ExpectedSourceSpan{
819 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
820 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
821 .{ .start_line = 3, .end_line = 3, .filename = "blah.rc" },
822 .{ .start_line = 4, .end_line = 4, .filename = "blah.rc" },
823 .{ .start_line = 5, .end_line = 5, .filename = "blah.rc" },
824 },
825 \\/*
826 \\#line 1 "irrelevant.rc"
827 \\
828 \\
829 \\*/
830 , .{ .initial_filename = "blah.rc" });
831}
lib/compiler/resinator/utils.zig created+122
...@@ -0,0 +1,122 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4/// Like std.io.FixedBufferStream but does no bounds checking
5pub const UncheckedSliceWriter = struct {
6 const Self = @This();
7
8 pos: usize = 0,
9 slice: []u8,
10
11 pub fn write(self: *Self, char: u8) void {
12 self.slice[self.pos] = char;
13 self.pos += 1;
14 }
15
16 pub fn writeSlice(self: *Self, slice: []const u8) void {
17 for (slice) |c| {
18 self.write(c);
19 }
20 }
21
22 pub fn getWritten(self: Self) []u8 {
23 return self.slice[0..self.pos];
24 }
25};
26
27/// Cross-platform 'std.fs.Dir.openFile' wrapper that will always return IsDir if
28/// a directory is attempted to be opened.
29/// TODO: Remove once https://github.com/ziglang/zig/issues/5732 is addressed.
30pub fn openFileNotDir(cwd: std.fs.Dir, path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File {
31 const file = try cwd.openFile(path, flags);
32 errdefer file.close();
33 // https://github.com/ziglang/zig/issues/5732
34 if (builtin.os.tag != .windows) {
35 const stat = try file.stat();
36
37 if (stat.kind == .directory)
38 return error.IsDir;
39 }
40 return file;
41}
42
43/// Emulates the Windows implementation of `iswdigit`, but only returns true
44/// for the non-ASCII digits that `iswdigit` on Windows would return true for.
45pub fn isNonAsciiDigit(c: u21) bool {
46 return switch (c) {
47 '²',
48 '³',
49 '¹',
50 '\u{660}'...'\u{669}',
51 '\u{6F0}'...'\u{6F9}',
52 '\u{7C0}'...'\u{7C9}',
53 '\u{966}'...'\u{96F}',
54 '\u{9E6}'...'\u{9EF}',
55 '\u{A66}'...'\u{A6F}',
56 '\u{AE6}'...'\u{AEF}',
57 '\u{B66}'...'\u{B6F}',
58 '\u{BE6}'...'\u{BEF}',
59 '\u{C66}'...'\u{C6F}',
60 '\u{CE6}'...'\u{CEF}',
61 '\u{D66}'...'\u{D6F}',
62 '\u{E50}'...'\u{E59}',
63 '\u{ED0}'...'\u{ED9}',
64 '\u{F20}'...'\u{F29}',
65 '\u{1040}'...'\u{1049}',
66 '\u{1090}'...'\u{1099}',
67 '\u{17E0}'...'\u{17E9}',
68 '\u{1810}'...'\u{1819}',
69 '\u{1946}'...'\u{194F}',
70 '\u{19D0}'...'\u{19D9}',
71 '\u{1B50}'...'\u{1B59}',
72 '\u{1BB0}'...'\u{1BB9}',
73 '\u{1C40}'...'\u{1C49}',
74 '\u{1C50}'...'\u{1C59}',
75 '\u{A620}'...'\u{A629}',
76 '\u{A8D0}'...'\u{A8D9}',
77 '\u{A900}'...'\u{A909}',
78 '\u{AA50}'...'\u{AA59}',
79 '\u{FF10}'...'\u{FF19}',
80 => true,
81 else => false,
82 };
83}
84
85/// Used for generic colored errors/warnings/notes, more context-specific error messages
86/// are handled elsewhere.
87pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type: enum { err, warning, note }, comptime format: []const u8, args: anytype) !void {
88 switch (msg_type) {
89 .err => {
90 try config.setColor(writer, .bold);
91 try config.setColor(writer, .red);
92 try writer.writeAll("error: ");
93 },
94 .warning => {
95 try config.setColor(writer, .bold);
96 try config.setColor(writer, .yellow);
97 try writer.writeAll("warning: ");
98 },
99 .note => {
100 try config.setColor(writer, .reset);
101 try config.setColor(writer, .cyan);
102 try writer.writeAll("note: ");
103 },
104 }
105 try config.setColor(writer, .reset);
106 if (msg_type == .err) {
107 try config.setColor(writer, .bold);
108 }
109 try writer.print(format, args);
110 try writer.writeByte('\n');
111 try config.setColor(writer, .reset);
112}
113
114pub fn isLineEndingPair(first: u8, second: u8) bool {
115 if (first != '\r' and first != '\n') return false;
116 if (second != '\r' and second != '\n') return false;
117
118 // can't be \n\n or \r\r
119 if (first == second) return false;
120
121 return true;
122}
lib/compiler/resinator/windows1252.zig created+588
...@@ -0,0 +1,588 @@
1const std = @import("std");
2
3pub fn windows1252ToUtf8Stream(writer: anytype, reader: anytype) !usize {
4 var bytes_written: usize = 0;
5 var utf8_buf: [3]u8 = undefined;
6 while (true) {
7 const c = reader.readByte() catch |err| switch (err) {
8 error.EndOfStream => return bytes_written,
9 else => |e| return e,
10 };
11 const codepoint = toCodepoint(c);
12 if (codepoint <= 0x7F) {
13 try writer.writeByte(c);
14 bytes_written += 1;
15 } else {
16 const utf8_len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch unreachable;
17 try writer.writeAll(utf8_buf[0..utf8_len]);
18 bytes_written += utf8_len;
19 }
20 }
21}
22
23/// Returns the number of code units written to the writer
24pub fn windows1252ToUtf16AllocZ(allocator: std.mem.Allocator, win1252_str: []const u8) ![:0]u16 {
25 // Guaranteed to need exactly the same number of code units as Windows-1252 bytes
26 var utf16_slice = try allocator.allocSentinel(u16, win1252_str.len, 0);
27 errdefer allocator.free(utf16_slice);
28 for (win1252_str, 0..) |c, i| {
29 utf16_slice[i] = toCodepoint(c);
30 }
31 return utf16_slice;
32}
33
34/// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt
35pub fn toCodepoint(c: u8) u16 {
36 return switch (c) {
37 0x80 => 0x20ac, // Euro Sign
38 0x82 => 0x201a, // Single Low-9 Quotation Mark
39 0x83 => 0x0192, // Latin Small Letter F With Hook
40 0x84 => 0x201e, // Double Low-9 Quotation Mark
41 0x85 => 0x2026, // Horizontal Ellipsis
42 0x86 => 0x2020, // Dagger
43 0x87 => 0x2021, // Double Dagger
44 0x88 => 0x02c6, // Modifier Letter Circumflex Accent
45 0x89 => 0x2030, // Per Mille Sign
46 0x8a => 0x0160, // Latin Capital Letter S With Caron
47 0x8b => 0x2039, // Single Left-Pointing Angle Quotation Mark
48 0x8c => 0x0152, // Latin Capital Ligature Oe
49 0x8e => 0x017d, // Latin Capital Letter Z With Caron
50 0x91 => 0x2018, // Left Single Quotation Mark
51 0x92 => 0x2019, // Right Single Quotation Mark
52 0x93 => 0x201c, // Left Double Quotation Mark
53 0x94 => 0x201d, // Right Double Quotation Mark
54 0x95 => 0x2022, // Bullet
55 0x96 => 0x2013, // En Dash
56 0x97 => 0x2014, // Em Dash
57 0x98 => 0x02dc, // Small Tilde
58 0x99 => 0x2122, // Trade Mark Sign
59 0x9a => 0x0161, // Latin Small Letter S With Caron
60 0x9b => 0x203a, // Single Right-Pointing Angle Quotation Mark
61 0x9c => 0x0153, // Latin Small Ligature Oe
62 0x9e => 0x017e, // Latin Small Letter Z With Caron
63 0x9f => 0x0178, // Latin Capital Letter Y With Diaeresis
64 else => c,
65 };
66}
67
68/// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt
69/// Plus some mappings found empirically by iterating all codepoints:
70/// 0x2007 => 0xA0, // Figure Space
71/// 0x2008 => ' ', // Punctuation Space
72/// 0x2009 => ' ', // Thin Space
73/// 0x200A => ' ', // Hair Space
74/// 0x2012 => '-', // Figure Dash
75/// 0x2015 => '-', // Horizontal Bar
76/// 0x201B => '\'', // Single High-reversed-9 Quotation Mark
77/// 0x201F => '"', // Double High-reversed-9 Quotation Mark
78/// 0x202F => 0xA0, // Narrow No-Break Space
79/// 0x2033 => '"', // Double Prime
80/// 0x2036 => '"', // Reversed Double Prime
81pub fn bestFitFromCodepoint(codepoint: u21) ?u8 {
82 return switch (codepoint) {
83 0x00...0x7F,
84 0x81,
85 0x8D,
86 0x8F,
87 0x90,
88 0x9D,
89 0xA0...0xFF,
90 => @intCast(codepoint),
91 0x0100 => 0x41, // Latin Capital Letter A With Macron
92 0x0101 => 0x61, // Latin Small Letter A With Macron
93 0x0102 => 0x41, // Latin Capital Letter A With Breve
94 0x0103 => 0x61, // Latin Small Letter A With Breve
95 0x0104 => 0x41, // Latin Capital Letter A With Ogonek
96 0x0105 => 0x61, // Latin Small Letter A With Ogonek
97 0x0106 => 0x43, // Latin Capital Letter C With Acute
98 0x0107 => 0x63, // Latin Small Letter C With Acute
99 0x0108 => 0x43, // Latin Capital Letter C With Circumflex
100 0x0109 => 0x63, // Latin Small Letter C With Circumflex
101 0x010a => 0x43, // Latin Capital Letter C With Dot Above
102 0x010b => 0x63, // Latin Small Letter C With Dot Above
103 0x010c => 0x43, // Latin Capital Letter C With Caron
104 0x010d => 0x63, // Latin Small Letter C With Caron
105 0x010e => 0x44, // Latin Capital Letter D With Caron
106 0x010f => 0x64, // Latin Small Letter D With Caron
107 0x0110 => 0xd0, // Latin Capital Letter D With Stroke
108 0x0111 => 0x64, // Latin Small Letter D With Stroke
109 0x0112 => 0x45, // Latin Capital Letter E With Macron
110 0x0113 => 0x65, // Latin Small Letter E With Macron
111 0x0114 => 0x45, // Latin Capital Letter E With Breve
112 0x0115 => 0x65, // Latin Small Letter E With Breve
113 0x0116 => 0x45, // Latin Capital Letter E With Dot Above
114 0x0117 => 0x65, // Latin Small Letter E With Dot Above
115 0x0118 => 0x45, // Latin Capital Letter E With Ogonek
116 0x0119 => 0x65, // Latin Small Letter E With Ogonek
117 0x011a => 0x45, // Latin Capital Letter E With Caron
118 0x011b => 0x65, // Latin Small Letter E With Caron
119 0x011c => 0x47, // Latin Capital Letter G With Circumflex
120 0x011d => 0x67, // Latin Small Letter G With Circumflex
121 0x011e => 0x47, // Latin Capital Letter G With Breve
122 0x011f => 0x67, // Latin Small Letter G With Breve
123 0x0120 => 0x47, // Latin Capital Letter G With Dot Above
124 0x0121 => 0x67, // Latin Small Letter G With Dot Above
125 0x0122 => 0x47, // Latin Capital Letter G With Cedilla
126 0x0123 => 0x67, // Latin Small Letter G With Cedilla
127 0x0124 => 0x48, // Latin Capital Letter H With Circumflex
128 0x0125 => 0x68, // Latin Small Letter H With Circumflex
129 0x0126 => 0x48, // Latin Capital Letter H With Stroke
130 0x0127 => 0x68, // Latin Small Letter H With Stroke
131 0x0128 => 0x49, // Latin Capital Letter I With Tilde
132 0x0129 => 0x69, // Latin Small Letter I With Tilde
133 0x012a => 0x49, // Latin Capital Letter I With Macron
134 0x012b => 0x69, // Latin Small Letter I With Macron
135 0x012c => 0x49, // Latin Capital Letter I With Breve
136 0x012d => 0x69, // Latin Small Letter I With Breve
137 0x012e => 0x49, // Latin Capital Letter I With Ogonek
138 0x012f => 0x69, // Latin Small Letter I With Ogonek
139 0x0130 => 0x49, // Latin Capital Letter I With Dot Above
140 0x0131 => 0x69, // Latin Small Letter Dotless I
141 0x0134 => 0x4a, // Latin Capital Letter J With Circumflex
142 0x0135 => 0x6a, // Latin Small Letter J With Circumflex
143 0x0136 => 0x4b, // Latin Capital Letter K With Cedilla
144 0x0137 => 0x6b, // Latin Small Letter K With Cedilla
145 0x0139 => 0x4c, // Latin Capital Letter L With Acute
146 0x013a => 0x6c, // Latin Small Letter L With Acute
147 0x013b => 0x4c, // Latin Capital Letter L With Cedilla
148 0x013c => 0x6c, // Latin Small Letter L With Cedilla
149 0x013d => 0x4c, // Latin Capital Letter L With Caron
150 0x013e => 0x6c, // Latin Small Letter L With Caron
151 0x0141 => 0x4c, // Latin Capital Letter L With Stroke
152 0x0142 => 0x6c, // Latin Small Letter L With Stroke
153 0x0143 => 0x4e, // Latin Capital Letter N With Acute
154 0x0144 => 0x6e, // Latin Small Letter N With Acute
155 0x0145 => 0x4e, // Latin Capital Letter N With Cedilla
156 0x0146 => 0x6e, // Latin Small Letter N With Cedilla
157 0x0147 => 0x4e, // Latin Capital Letter N With Caron
158 0x0148 => 0x6e, // Latin Small Letter N With Caron
159 0x014c => 0x4f, // Latin Capital Letter O With Macron
160 0x014d => 0x6f, // Latin Small Letter O With Macron
161 0x014e => 0x4f, // Latin Capital Letter O With Breve
162 0x014f => 0x6f, // Latin Small Letter O With Breve
163 0x0150 => 0x4f, // Latin Capital Letter O With Double Acute
164 0x0151 => 0x6f, // Latin Small Letter O With Double Acute
165 0x0152 => 0x8c, // Latin Capital Ligature Oe
166 0x0153 => 0x9c, // Latin Small Ligature Oe
167 0x0154 => 0x52, // Latin Capital Letter R With Acute
168 0x0155 => 0x72, // Latin Small Letter R With Acute
169 0x0156 => 0x52, // Latin Capital Letter R With Cedilla
170 0x0157 => 0x72, // Latin Small Letter R With Cedilla
171 0x0158 => 0x52, // Latin Capital Letter R With Caron
172 0x0159 => 0x72, // Latin Small Letter R With Caron
173 0x015a => 0x53, // Latin Capital Letter S With Acute
174 0x015b => 0x73, // Latin Small Letter S With Acute
175 0x015c => 0x53, // Latin Capital Letter S With Circumflex
176 0x015d => 0x73, // Latin Small Letter S With Circumflex
177 0x015e => 0x53, // Latin Capital Letter S With Cedilla
178 0x015f => 0x73, // Latin Small Letter S With Cedilla
179 0x0160 => 0x8a, // Latin Capital Letter S With Caron
180 0x0161 => 0x9a, // Latin Small Letter S With Caron
181 0x0162 => 0x54, // Latin Capital Letter T With Cedilla
182 0x0163 => 0x74, // Latin Small Letter T With Cedilla
183 0x0164 => 0x54, // Latin Capital Letter T With Caron
184 0x0165 => 0x74, // Latin Small Letter T With Caron
185 0x0166 => 0x54, // Latin Capital Letter T With Stroke
186 0x0167 => 0x74, // Latin Small Letter T With Stroke
187 0x0168 => 0x55, // Latin Capital Letter U With Tilde
188 0x0169 => 0x75, // Latin Small Letter U With Tilde
189 0x016a => 0x55, // Latin Capital Letter U With Macron
190 0x016b => 0x75, // Latin Small Letter U With Macron
191 0x016c => 0x55, // Latin Capital Letter U With Breve
192 0x016d => 0x75, // Latin Small Letter U With Breve
193 0x016e => 0x55, // Latin Capital Letter U With Ring Above
194 0x016f => 0x75, // Latin Small Letter U With Ring Above
195 0x0170 => 0x55, // Latin Capital Letter U With Double Acute
196 0x0171 => 0x75, // Latin Small Letter U With Double Acute
197 0x0172 => 0x55, // Latin Capital Letter U With Ogonek
198 0x0173 => 0x75, // Latin Small Letter U With Ogonek
199 0x0174 => 0x57, // Latin Capital Letter W With Circumflex
200 0x0175 => 0x77, // Latin Small Letter W With Circumflex
201 0x0176 => 0x59, // Latin Capital Letter Y With Circumflex
202 0x0177 => 0x79, // Latin Small Letter Y With Circumflex
203 0x0178 => 0x9f, // Latin Capital Letter Y With Diaeresis
204 0x0179 => 0x5a, // Latin Capital Letter Z With Acute
205 0x017a => 0x7a, // Latin Small Letter Z With Acute
206 0x017b => 0x5a, // Latin Capital Letter Z With Dot Above
207 0x017c => 0x7a, // Latin Small Letter Z With Dot Above
208 0x017d => 0x8e, // Latin Capital Letter Z With Caron
209 0x017e => 0x9e, // Latin Small Letter Z With Caron
210 0x0180 => 0x62, // Latin Small Letter B With Stroke
211 0x0189 => 0xd0, // Latin Capital Letter African D
212 0x0191 => 0x83, // Latin Capital Letter F With Hook
213 0x0192 => 0x83, // Latin Small Letter F With Hook
214 0x0197 => 0x49, // Latin Capital Letter I With Stroke
215 0x019a => 0x6c, // Latin Small Letter L With Bar
216 0x019f => 0x4f, // Latin Capital Letter O With Middle Tilde
217 0x01a0 => 0x4f, // Latin Capital Letter O With Horn
218 0x01a1 => 0x6f, // Latin Small Letter O With Horn
219 0x01ab => 0x74, // Latin Small Letter T With Palatal Hook
220 0x01ae => 0x54, // Latin Capital Letter T With Retroflex Hook
221 0x01af => 0x55, // Latin Capital Letter U With Horn
222 0x01b0 => 0x75, // Latin Small Letter U With Horn
223 0x01b6 => 0x7a, // Latin Small Letter Z With Stroke
224 0x01c0 => 0x7c, // Latin Letter Dental Click
225 0x01c3 => 0x21, // Latin Letter Retroflex Click
226 0x01cd => 0x41, // Latin Capital Letter A With Caron
227 0x01ce => 0x61, // Latin Small Letter A With Caron
228 0x01cf => 0x49, // Latin Capital Letter I With Caron
229 0x01d0 => 0x69, // Latin Small Letter I With Caron
230 0x01d1 => 0x4f, // Latin Capital Letter O With Caron
231 0x01d2 => 0x6f, // Latin Small Letter O With Caron
232 0x01d3 => 0x55, // Latin Capital Letter U With Caron
233 0x01d4 => 0x75, // Latin Small Letter U With Caron
234 0x01d5 => 0x55, // Latin Capital Letter U With Diaeresis And Macron
235 0x01d6 => 0x75, // Latin Small Letter U With Diaeresis And Macron
236 0x01d7 => 0x55, // Latin Capital Letter U With Diaeresis And Acute
237 0x01d8 => 0x75, // Latin Small Letter U With Diaeresis And Acute
238 0x01d9 => 0x55, // Latin Capital Letter U With Diaeresis And Caron
239 0x01da => 0x75, // Latin Small Letter U With Diaeresis And Caron
240 0x01db => 0x55, // Latin Capital Letter U With Diaeresis And Grave
241 0x01dc => 0x75, // Latin Small Letter U With Diaeresis And Grave
242 0x01de => 0x41, // Latin Capital Letter A With Diaeresis And Macron
243 0x01df => 0x61, // Latin Small Letter A With Diaeresis And Macron
244 0x01e4 => 0x47, // Latin Capital Letter G With Stroke
245 0x01e5 => 0x67, // Latin Small Letter G With Stroke
246 0x01e6 => 0x47, // Latin Capital Letter G With Caron
247 0x01e7 => 0x67, // Latin Small Letter G With Caron
248 0x01e8 => 0x4b, // Latin Capital Letter K With Caron
249 0x01e9 => 0x6b, // Latin Small Letter K With Caron
250 0x01ea => 0x4f, // Latin Capital Letter O With Ogonek
251 0x01eb => 0x6f, // Latin Small Letter O With Ogonek
252 0x01ec => 0x4f, // Latin Capital Letter O With Ogonek And Macron
253 0x01ed => 0x6f, // Latin Small Letter O With Ogonek And Macron
254 0x01f0 => 0x6a, // Latin Small Letter J With Caron
255 0x0261 => 0x67, // Latin Small Letter Script G
256 0x02b9 => 0x27, // Modifier Letter Prime
257 0x02ba => 0x22, // Modifier Letter Double Prime
258 0x02bc => 0x27, // Modifier Letter Apostrophe
259 0x02c4 => 0x5e, // Modifier Letter Up Arrowhead
260 0x02c6 => 0x88, // Modifier Letter Circumflex Accent
261 0x02c8 => 0x27, // Modifier Letter Vertical Line
262 0x02c9 => 0xaf, // Modifier Letter Macron
263 0x02ca => 0xb4, // Modifier Letter Acute Accent
264 0x02cb => 0x60, // Modifier Letter Grave Accent
265 0x02cd => 0x5f, // Modifier Letter Low Macron
266 0x02da => 0xb0, // Ring Above
267 0x02dc => 0x98, // Small Tilde
268 0x0300 => 0x60, // Combining Grave Accent
269 0x0301 => 0xb4, // Combining Acute Accent
270 0x0302 => 0x5e, // Combining Circumflex Accent
271 0x0303 => 0x7e, // Combining Tilde
272 0x0304 => 0xaf, // Combining Macron
273 0x0305 => 0xaf, // Combining Overline
274 0x0308 => 0xa8, // Combining Diaeresis
275 0x030a => 0xb0, // Combining Ring Above
276 0x030e => 0x22, // Combining Double Vertical Line Above
277 0x0327 => 0xb8, // Combining Cedilla
278 0x0331 => 0x5f, // Combining Macron Below
279 0x0332 => 0x5f, // Combining Low Line
280 0x037e => 0x3b, // Greek Question Mark
281 0x0393 => 0x47, // Greek Capital Letter Gamma
282 0x0398 => 0x54, // Greek Capital Letter Theta
283 0x03a3 => 0x53, // Greek Capital Letter Sigma
284 0x03a6 => 0x46, // Greek Capital Letter Phi
285 0x03a9 => 0x4f, // Greek Capital Letter Omega
286 0x03b1 => 0x61, // Greek Small Letter Alpha
287 0x03b2 => 0xdf, // Greek Small Letter Beta
288 0x03b4 => 0x64, // Greek Small Letter Delta
289 0x03b5 => 0x65, // Greek Small Letter Epsilon
290 0x03bc => 0xb5, // Greek Small Letter Mu
291 0x03c0 => 0x70, // Greek Small Letter Pi
292 0x03c3 => 0x73, // Greek Small Letter Sigma
293 0x03c4 => 0x74, // Greek Small Letter Tau
294 0x03c6 => 0x66, // Greek Small Letter Phi
295 0x04bb => 0x68, // Cyrillic Small Letter Shha
296 0x0589 => 0x3a, // Armenian Full Stop
297 0x066a => 0x25, // Arabic Percent Sign
298 0x2000 => 0x20, // En Quad
299 0x2001 => 0x20, // Em Quad
300 0x2002 => 0x20, // En Space
301 0x2003 => 0x20, // Em Space
302 0x2004 => 0x20, // Three-Per-Em Space
303 0x2005 => 0x20, // Four-Per-Em Space
304 0x2006 => 0x20, // Six-Per-Em Space
305 0x2010 => 0x2d, // Hyphen
306 0x2011 => 0x2d, // Non-Breaking Hyphen
307 0x2013 => 0x96, // En Dash
308 0x2014 => 0x97, // Em Dash
309 0x2017 => 0x3d, // Double Low Line
310 0x2018 => 0x91, // Left Single Quotation Mark
311 0x2019 => 0x92, // Right Single Quotation Mark
312 0x201a => 0x82, // Single Low-9 Quotation Mark
313 0x201c => 0x93, // Left Double Quotation Mark
314 0x201d => 0x94, // Right Double Quotation Mark
315 0x201e => 0x84, // Double Low-9 Quotation Mark
316 0x2020 => 0x86, // Dagger
317 0x2021 => 0x87, // Double Dagger
318 0x2022 => 0x95, // Bullet
319 0x2024 => 0xb7, // One Dot Leader
320 0x2026 => 0x85, // Horizontal Ellipsis
321 0x2030 => 0x89, // Per Mille Sign
322 0x2032 => 0x27, // Prime
323 0x2035 => 0x60, // Reversed Prime
324 0x2039 => 0x8b, // Single Left-Pointing Angle Quotation Mark
325 0x203a => 0x9b, // Single Right-Pointing Angle Quotation Mark
326 0x2044 => 0x2f, // Fraction Slash
327 0x2070 => 0xb0, // Superscript Zero
328 0x2074 => 0x34, // Superscript Four
329 0x2075 => 0x35, // Superscript Five
330 0x2076 => 0x36, // Superscript Six
331 0x2077 => 0x37, // Superscript Seven
332 0x2078 => 0x38, // Superscript Eight
333 0x207f => 0x6e, // Superscript Latin Small Letter N
334 0x2080 => 0x30, // Subscript Zero
335 0x2081 => 0x31, // Subscript One
336 0x2082 => 0x32, // Subscript Two
337 0x2083 => 0x33, // Subscript Three
338 0x2084 => 0x34, // Subscript Four
339 0x2085 => 0x35, // Subscript Five
340 0x2086 => 0x36, // Subscript Six
341 0x2087 => 0x37, // Subscript Seven
342 0x2088 => 0x38, // Subscript Eight
343 0x2089 => 0x39, // Subscript Nine
344 0x20ac => 0x80, // Euro Sign
345 0x20a1 => 0xa2, // Colon Sign
346 0x20a4 => 0xa3, // Lira Sign
347 0x20a7 => 0x50, // Peseta Sign
348 0x2102 => 0x43, // Double-Struck Capital C
349 0x2107 => 0x45, // Euler Constant
350 0x210a => 0x67, // Script Small G
351 0x210b => 0x48, // Script Capital H
352 0x210c => 0x48, // Black-Letter Capital H
353 0x210d => 0x48, // Double-Struck Capital H
354 0x210e => 0x68, // Planck Constant
355 0x2110 => 0x49, // Script Capital I
356 0x2111 => 0x49, // Black-Letter Capital I
357 0x2112 => 0x4c, // Script Capital L
358 0x2113 => 0x6c, // Script Small L
359 0x2115 => 0x4e, // Double-Struck Capital N
360 0x2118 => 0x50, // Script Capital P
361 0x2119 => 0x50, // Double-Struck Capital P
362 0x211a => 0x51, // Double-Struck Capital Q
363 0x211b => 0x52, // Script Capital R
364 0x211c => 0x52, // Black-Letter Capital R
365 0x211d => 0x52, // Double-Struck Capital R
366 0x2122 => 0x99, // Trade Mark Sign
367 0x2124 => 0x5a, // Double-Struck Capital Z
368 0x2128 => 0x5a, // Black-Letter Capital Z
369 0x212a => 0x4b, // Kelvin Sign
370 0x212b => 0xc5, // Angstrom Sign
371 0x212c => 0x42, // Script Capital B
372 0x212d => 0x43, // Black-Letter Capital C
373 0x212e => 0x65, // Estimated Symbol
374 0x212f => 0x65, // Script Small E
375 0x2130 => 0x45, // Script Capital E
376 0x2131 => 0x46, // Script Capital F
377 0x2133 => 0x4d, // Script Capital M
378 0x2134 => 0x6f, // Script Small O
379 0x2205 => 0xd8, // Empty Set
380 0x2212 => 0x2d, // Minus Sign
381 0x2213 => 0xb1, // Minus-Or-Plus Sign
382 0x2215 => 0x2f, // Division Slash
383 0x2216 => 0x5c, // Set Minus
384 0x2217 => 0x2a, // Asterisk Operator
385 0x2218 => 0xb0, // Ring Operator
386 0x2219 => 0xb7, // Bullet Operator
387 0x221a => 0x76, // Square Root
388 0x221e => 0x38, // Infinity
389 0x2223 => 0x7c, // Divides
390 0x2229 => 0x6e, // Intersection
391 0x2236 => 0x3a, // Ratio
392 0x223c => 0x7e, // Tilde Operator
393 0x2248 => 0x98, // Almost Equal To
394 0x2261 => 0x3d, // Identical To
395 0x2264 => 0x3d, // Less-Than Or Equal To
396 0x2265 => 0x3d, // Greater-Than Or Equal To
397 0x226a => 0xab, // Much Less-Than
398 0x226b => 0xbb, // Much Greater-Than
399 0x22c5 => 0xb7, // Dot Operator
400 0x2302 => 0xa6, // House
401 0x2303 => 0x5e, // Up Arrowhead
402 0x2310 => 0xac, // Reversed Not Sign
403 0x2320 => 0x28, // Top Half Integral
404 0x2321 => 0x29, // Bottom Half Integral
405 0x2329 => 0x3c, // Left-Pointing Angle Bracket
406 0x232a => 0x3e, // Right-Pointing Angle Bracket
407 0x2500 => 0x2d, // Box Drawings Light Horizontal
408 0x2502 => 0xa6, // Box Drawings Light Vertical
409 0x250c => 0x2b, // Box Drawings Light Down And Right
410 0x2510 => 0x2b, // Box Drawings Light Down And Left
411 0x2514 => 0x2b, // Box Drawings Light Up And Right
412 0x2518 => 0x2b, // Box Drawings Light Up And Left
413 0x251c => 0x2b, // Box Drawings Light Vertical And Right
414 0x2524 => 0xa6, // Box Drawings Light Vertical And Left
415 0x252c => 0x2d, // Box Drawings Light Down And Horizontal
416 0x2534 => 0x2d, // Box Drawings Light Up And Horizontal
417 0x253c => 0x2b, // Box Drawings Light Vertical And Horizontal
418 0x2550 => 0x2d, // Box Drawings Double Horizontal
419 0x2551 => 0xa6, // Box Drawings Double Vertical
420 0x2552 => 0x2b, // Box Drawings Down Single And Right Double
421 0x2553 => 0x2b, // Box Drawings Down Double And Right Single
422 0x2554 => 0x2b, // Box Drawings Double Down And Right
423 0x2555 => 0x2b, // Box Drawings Down Single And Left Double
424 0x2556 => 0x2b, // Box Drawings Down Double And Left Single
425 0x2557 => 0x2b, // Box Drawings Double Down And Left
426 0x2558 => 0x2b, // Box Drawings Up Single And Right Double
427 0x2559 => 0x2b, // Box Drawings Up Double And Right Single
428 0x255a => 0x2b, // Box Drawings Double Up And Right
429 0x255b => 0x2b, // Box Drawings Up Single And Left Double
430 0x255c => 0x2b, // Box Drawings Up Double And Left Single
431 0x255d => 0x2b, // Box Drawings Double Up And Left
432 0x255e => 0xa6, // Box Drawings Vertical Single And Right Double
433 0x255f => 0xa6, // Box Drawings Vertical Double And Right Single
434 0x2560 => 0xa6, // Box Drawings Double Vertical And Right
435 0x2561 => 0xa6, // Box Drawings Vertical Single And Left Double
436 0x2562 => 0xa6, // Box Drawings Vertical Double And Left Single
437 0x2563 => 0xa6, // Box Drawings Double Vertical And Left
438 0x2564 => 0x2d, // Box Drawings Down Single And Horizontal Double
439 0x2565 => 0x2d, // Box Drawings Down Double And Horizontal Single
440 0x2566 => 0x2d, // Box Drawings Double Down And Horizontal
441 0x2567 => 0x2d, // Box Drawings Up Single And Horizontal Double
442 0x2568 => 0x2d, // Box Drawings Up Double And Horizontal Single
443 0x2569 => 0x2d, // Box Drawings Double Up And Horizontal
444 0x256a => 0x2b, // Box Drawings Vertical Single And Horizontal Double
445 0x256b => 0x2b, // Box Drawings Vertical Double And Horizontal Single
446 0x256c => 0x2b, // Box Drawings Double Vertical And Horizontal
447 0x2580 => 0xaf, // Upper Half Block
448 0x2584 => 0x5f, // Lower Half Block
449 0x2588 => 0xa6, // Full Block
450 0x258c => 0xa6, // Left Half Block
451 0x2590 => 0xa6, // Right Half Block
452 0x2591 => 0xa6, // Light Shade
453 0x2592 => 0xa6, // Medium Shade
454 0x2593 => 0xa6, // Dark Shade
455 0x25a0 => 0xa6, // Black Square
456 0x263c => 0xa4, // White Sun With Rays
457 0x2758 => 0x7c, // Light Vertical Bar
458 0x3000 => 0x20, // Ideographic Space
459 0x3008 => 0x3c, // Left Angle Bracket
460 0x3009 => 0x3e, // Right Angle Bracket
461 0x300a => 0xab, // Left Double Angle Bracket
462 0x300b => 0xbb, // Right Double Angle Bracket
463 0x301a => 0x5b, // Left White Square Bracket
464 0x301b => 0x5d, // Right White Square Bracket
465 0x30fb => 0xb7, // Katakana Middle Dot
466 0xff01 => 0x21, // Fullwidth Exclamation Mark
467 0xff02 => 0x22, // Fullwidth Quotation Mark
468 0xff03 => 0x23, // Fullwidth Number Sign
469 0xff04 => 0x24, // Fullwidth Dollar Sign
470 0xff05 => 0x25, // Fullwidth Percent Sign
471 0xff06 => 0x26, // Fullwidth Ampersand
472 0xff07 => 0x27, // Fullwidth Apostrophe
473 0xff08 => 0x28, // Fullwidth Left Parenthesis
474 0xff09 => 0x29, // Fullwidth Right Parenthesis
475 0xff0a => 0x2a, // Fullwidth Asterisk
476 0xff0b => 0x2b, // Fullwidth Plus Sign
477 0xff0c => 0x2c, // Fullwidth Comma
478 0xff0d => 0x2d, // Fullwidth Hyphen-Minus
479 0xff0e => 0x2e, // Fullwidth Full Stop
480 0xff0f => 0x2f, // Fullwidth Solidus
481 0xff10 => 0x30, // Fullwidth Digit Zero
482 0xff11 => 0x31, // Fullwidth Digit One
483 0xff12 => 0x32, // Fullwidth Digit Two
484 0xff13 => 0x33, // Fullwidth Digit Three
485 0xff14 => 0x34, // Fullwidth Digit Four
486 0xff15 => 0x35, // Fullwidth Digit Five
487 0xff16 => 0x36, // Fullwidth Digit Six
488 0xff17 => 0x37, // Fullwidth Digit Seven
489 0xff18 => 0x38, // Fullwidth Digit Eight
490 0xff19 => 0x39, // Fullwidth Digit Nine
491 0xff1a => 0x3a, // Fullwidth Colon
492 0xff1b => 0x3b, // Fullwidth Semicolon
493 0xff1c => 0x3c, // Fullwidth Less-Than Sign
494 0xff1d => 0x3d, // Fullwidth Equals Sign
495 0xff1e => 0x3e, // Fullwidth Greater-Than Sign
496 0xff1f => 0x3f, // Fullwidth Question Mark
497 0xff20 => 0x40, // Fullwidth Commercial At
498 0xff21 => 0x41, // Fullwidth Latin Capital Letter A
499 0xff22 => 0x42, // Fullwidth Latin Capital Letter B
500 0xff23 => 0x43, // Fullwidth Latin Capital Letter C
501 0xff24 => 0x44, // Fullwidth Latin Capital Letter D
502 0xff25 => 0x45, // Fullwidth Latin Capital Letter E
503 0xff26 => 0x46, // Fullwidth Latin Capital Letter F
504 0xff27 => 0x47, // Fullwidth Latin Capital Letter G
505 0xff28 => 0x48, // Fullwidth Latin Capital Letter H
506 0xff29 => 0x49, // Fullwidth Latin Capital Letter I
507 0xff2a => 0x4a, // Fullwidth Latin Capital Letter J
508 0xff2b => 0x4b, // Fullwidth Latin Capital Letter K
509 0xff2c => 0x4c, // Fullwidth Latin Capital Letter L
510 0xff2d => 0x4d, // Fullwidth Latin Capital Letter M
511 0xff2e => 0x4e, // Fullwidth Latin Capital Letter N
512 0xff2f => 0x4f, // Fullwidth Latin Capital Letter O
513 0xff30 => 0x50, // Fullwidth Latin Capital Letter P
514 0xff31 => 0x51, // Fullwidth Latin Capital Letter Q
515 0xff32 => 0x52, // Fullwidth Latin Capital Letter R
516 0xff33 => 0x53, // Fullwidth Latin Capital Letter S
517 0xff34 => 0x54, // Fullwidth Latin Capital Letter T
518 0xff35 => 0x55, // Fullwidth Latin Capital Letter U
519 0xff36 => 0x56, // Fullwidth Latin Capital Letter V
520 0xff37 => 0x57, // Fullwidth Latin Capital Letter W
521 0xff38 => 0x58, // Fullwidth Latin Capital Letter X
522 0xff39 => 0x59, // Fullwidth Latin Capital Letter Y
523 0xff3a => 0x5a, // Fullwidth Latin Capital Letter Z
524 0xff3b => 0x5b, // Fullwidth Left Square Bracket
525 0xff3c => 0x5c, // Fullwidth Reverse Solidus
526 0xff3d => 0x5d, // Fullwidth Right Square Bracket
527 0xff3e => 0x5e, // Fullwidth Circumflex Accent
528 0xff3f => 0x5f, // Fullwidth Low Line
529 0xff40 => 0x60, // Fullwidth Grave Accent
530 0xff41 => 0x61, // Fullwidth Latin Small Letter A
531 0xff42 => 0x62, // Fullwidth Latin Small Letter B
532 0xff43 => 0x63, // Fullwidth Latin Small Letter C
533 0xff44 => 0x64, // Fullwidth Latin Small Letter D
534 0xff45 => 0x65, // Fullwidth Latin Small Letter E
535 0xff46 => 0x66, // Fullwidth Latin Small Letter F
536 0xff47 => 0x67, // Fullwidth Latin Small Letter G
537 0xff48 => 0x68, // Fullwidth Latin Small Letter H
538 0xff49 => 0x69, // Fullwidth Latin Small Letter I
539 0xff4a => 0x6a, // Fullwidth Latin Small Letter J
540 0xff4b => 0x6b, // Fullwidth Latin Small Letter K
541 0xff4c => 0x6c, // Fullwidth Latin Small Letter L
542 0xff4d => 0x6d, // Fullwidth Latin Small Letter M
543 0xff4e => 0x6e, // Fullwidth Latin Small Letter N
544 0xff4f => 0x6f, // Fullwidth Latin Small Letter O
545 0xff50 => 0x70, // Fullwidth Latin Small Letter P
546 0xff51 => 0x71, // Fullwidth Latin Small Letter Q
547 0xff52 => 0x72, // Fullwidth Latin Small Letter R
548 0xff53 => 0x73, // Fullwidth Latin Small Letter S
549 0xff54 => 0x74, // Fullwidth Latin Small Letter T
550 0xff55 => 0x75, // Fullwidth Latin Small Letter U
551 0xff56 => 0x76, // Fullwidth Latin Small Letter V
552 0xff57 => 0x77, // Fullwidth Latin Small Letter W
553 0xff58 => 0x78, // Fullwidth Latin Small Letter X
554 0xff59 => 0x79, // Fullwidth Latin Small Letter Y
555 0xff5a => 0x7a, // Fullwidth Latin Small Letter Z
556 0xff5b => 0x7b, // Fullwidth Left Curly Bracket
557 0xff5c => 0x7c, // Fullwidth Vertical Line
558 0xff5d => 0x7d, // Fullwidth Right Curly Bracket
559 0xff5e => 0x7e, // Fullwidth Tilde
560 // Not in the best fit mapping, but RC uses these mappings too
561 0x2007 => 0xA0, // Figure Space
562 0x2008 => ' ', // Punctuation Space
563 0x2009 => ' ', // Thin Space
564 0x200A => ' ', // Hair Space
565 0x2012 => '-', // Figure Dash
566 0x2015 => '-', // Horizontal Bar
567 0x201B => '\'', // Single High-reversed-9 Quotation Mark
568 0x201F => '"', // Double High-reversed-9 Quotation Mark
569 0x202F => 0xA0, // Narrow No-Break Space
570 0x2033 => '"', // Double Prime
571 0x2036 => '"', // Reversed Double Prime
572 else => null,
573 };
574}
575
576test "windows-1252 to utf8" {
577 var buf = std.ArrayList(u8).init(std.testing.allocator);
578 defer buf.deinit();
579
580 const input_windows1252 = "\x81pqrstuvwxyz{|}~\x80\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8e\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9e\x9f\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff";
581 const expected_utf8 = "\xc2\x81pqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ";
582
583 var fbs = std.io.fixedBufferStream(input_windows1252);
584 const bytes_written = try windows1252ToUtf8Stream(buf.writer(), fbs.reader());
585
586 try std.testing.expectEqualStrings(expected_utf8, buf.items);
587 try std.testing.expectEqual(expected_utf8.len, bytes_written);
588}
src/Compilation.zig+121-445
...@@ -36,7 +36,6 @@ const Cache = std.Build.Cache;...@@ -36,7 +36,6 @@ const Cache = std.Build.Cache;
36const c_codegen = @import("codegen/c.zig");36const c_codegen = @import("codegen/c.zig");
37const libtsan = @import("libtsan.zig");37const libtsan = @import("libtsan.zig");
38const Zir = std.zig.Zir;38const Zir = std.zig.Zir;
39const resinator = @import("resinator.zig");
40const Builtin = @import("Builtin.zig");39const Builtin = @import("Builtin.zig");
41const LlvmObject = @import("codegen/llvm.zig").Object;40const LlvmObject = @import("codegen/llvm.zig").Object;
4241
...@@ -174,7 +173,7 @@ local_cache_directory: Directory,...@@ -174,7 +173,7 @@ local_cache_directory: Directory,
174global_cache_directory: Directory,173global_cache_directory: Directory,
175libc_include_dir_list: []const []const u8,174libc_include_dir_list: []const []const u8,
176libc_framework_dir_list: []const []const u8,175libc_framework_dir_list: []const []const u8,
177rc_include_dir_list: []const []const u8,176rc_includes: RcIncludes,
178thread_pool: *ThreadPool,177thread_pool: *ThreadPool,
179178
180/// Populated when we build the libc++ static library. A Job to build this is placed in the queue179/// Populated when we build the libc++ static library. A Job to build this is placed in the queue
...@@ -1243,68 +1242,6 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil...@@ -1243,68 +1242,6 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
1243 options.libc_installation,1242 options.libc_installation,
1244 );1243 );
12451244
1246 // The include directories used when preprocessing .rc files are separate from the
1247 // target. Which include directories are used is determined by `options.rc_includes`.
1248 //
1249 // Note: It should be okay that the include directories used when compiling .rc
1250 // files differ from the include directories used when compiling the main
1251 // binary, since the .res format is not dependent on anything ABI-related. The
1252 // only relevant differences would be things like `#define` constants being
1253 // different in the MinGW headers vs the MSVC headers, but any such
1254 // differences would likely be a MinGW bug.
1255 const rc_dirs: std.zig.LibCDirs = b: {
1256 // Set the includes to .none here when there are no rc files to compile
1257 var includes = if (options.rc_source_files.len > 0) options.rc_includes else .none;
1258 const target = options.root_mod.resolved_target.result;
1259 if (!options.root_mod.resolved_target.is_native_os or target.os.tag != .windows) {
1260 switch (includes) {
1261 // MSVC can't be found when the host isn't Windows, so short-circuit.
1262 .msvc => return error.WindowsSdkNotFound,
1263 // Skip straight to gnu since we won't be able to detect
1264 // MSVC on non-Windows hosts.
1265 .any => includes = .gnu,
1266 .none, .gnu => {},
1267 }
1268 }
1269 while (true) switch (includes) {
1270 .any, .msvc => break :b std.zig.LibCDirs.detect(
1271 arena,
1272 options.zig_lib_directory.path.?,
1273 .{
1274 .cpu = target.cpu,
1275 .os = target.os,
1276 .abi = .msvc,
1277 .ofmt = target.ofmt,
1278 },
1279 options.root_mod.resolved_target.is_native_abi,
1280 // The .rc preprocessor will need to know the libc include dirs even if we
1281 // are not linking libc, so force 'link_libc' to true
1282 true,
1283 options.libc_installation,
1284 ) catch |err| {
1285 if (includes == .any) {
1286 // fall back to mingw
1287 includes = .gnu;
1288 continue;
1289 }
1290 return err;
1291 },
1292 .gnu => break :b try std.zig.LibCDirs.detectFromBuilding(arena, options.zig_lib_directory.path.?, .{
1293 .cpu = target.cpu,
1294 .os = target.os,
1295 .abi = .gnu,
1296 .ofmt = target.ofmt,
1297 }),
1298 .none => break :b .{
1299 .libc_include_dir_list = &[0][]u8{},
1300 .libc_installation = null,
1301 .libc_framework_dir_list = &.{},
1302 .sysroot = null,
1303 .darwin_sdk_layout = null,
1304 },
1305 };
1306 };
1307
1308 const sysroot = options.sysroot orelse libc_dirs.sysroot;1245 const sysroot = options.sysroot orelse libc_dirs.sysroot;
13091246
1310 const include_compiler_rt = options.want_compiler_rt orelse1247 const include_compiler_rt = options.want_compiler_rt orelse
...@@ -1492,7 +1429,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil...@@ -1492,7 +1429,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
1492 .self_exe_path = options.self_exe_path,1429 .self_exe_path = options.self_exe_path,
1493 .libc_include_dir_list = libc_dirs.libc_include_dir_list,1430 .libc_include_dir_list = libc_dirs.libc_include_dir_list,
1494 .libc_framework_dir_list = libc_dirs.libc_framework_dir_list,1431 .libc_framework_dir_list = libc_dirs.libc_framework_dir_list,
1495 .rc_include_dir_list = rc_dirs.libc_include_dir_list,1432 .rc_includes = options.rc_includes,
1496 .thread_pool = options.thread_pool,1433 .thread_pool = options.thread_pool,
1497 .clang_passthrough_mode = options.clang_passthrough_mode,1434 .clang_passthrough_mode = options.clang_passthrough_mode,
1498 .clang_preprocessor_mode = options.clang_preprocessor_mode,1435 .clang_preprocessor_mode = options.clang_preprocessor_mode,
...@@ -2506,7 +2443,7 @@ fn addNonIncrementalStuffToCacheManifest(...@@ -2506,7 +2443,7 @@ fn addNonIncrementalStuffToCacheManifest(
2506 man.hash.add(comp.link_eh_frame_hdr);2443 man.hash.add(comp.link_eh_frame_hdr);
2507 man.hash.add(comp.skip_linker_dependencies);2444 man.hash.add(comp.skip_linker_dependencies);
2508 man.hash.add(comp.include_compiler_rt);2445 man.hash.add(comp.include_compiler_rt);
2509 man.hash.addListOfBytes(comp.rc_include_dir_list);2446 man.hash.add(comp.rc_includes);
2510 man.hash.addListOfBytes(comp.force_undefined_symbols.keys());2447 man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
2511 man.hash.addListOfBytes(comp.framework_dirs);2448 man.hash.addListOfBytes(comp.framework_dirs);
2512 try link.hashAddSystemLibs(man, comp.system_libs);2449 try link.hashAddSystemLibs(man, comp.system_libs);
...@@ -4172,7 +4109,7 @@ pub fn obtainCObjectCacheManifest(...@@ -4172,7 +4109,7 @@ pub fn obtainCObjectCacheManifest(
4172pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest {4109pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest {
4173 var man = comp.cache_parent.obtain();4110 var man = comp.cache_parent.obtain();
41744111
4175 man.hash.addListOfBytes(comp.rc_include_dir_list);4112 man.hash.add(comp.rc_includes);
41764113
4177 return man;4114 return man;
4178}4115}
...@@ -4812,11 +4749,12 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P...@@ -4812,11 +4749,12 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
4812}4749}
48134750
4814fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void {4751fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void {
4815 if (!build_options.have_llvm) {4752 if (!std.process.can_spawn) {
4816 return comp.failWin32Resource(win32_resource, "clang not available: compiler built without LLVM extensions", .{});4753 return comp.failWin32Resource(win32_resource, "{s} does not support spawning a child process", .{@tagName(builtin.os.tag)});
4817 }4754 }
4755
4818 const self_exe_path = comp.self_exe_path orelse4756 const self_exe_path = comp.self_exe_path orelse
4819 return comp.failWin32Resource(win32_resource, "clang compilation disabled", .{});4757 return comp.failWin32Resource(win32_resource, "unable to find self exe path", .{});
48204758
4821 const tracy_trace = trace(@src());4759 const tracy_trace = trace(@src());
4822 defer tracy_trace.end();4760 defer tracy_trace.end();
...@@ -4856,6 +4794,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32...@@ -4856,6 +4794,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
4856 if (win32_resource.src == .manifest) {4794 if (win32_resource.src == .manifest) {
4857 _ = try man.addFile(src_path, null);4795 _ = try man.addFile(src_path, null);
48584796
4797 const rc_basename = try std.fmt.allocPrint(arena, "{s}.rc", .{src_basename});
4859 const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{src_basename});4798 const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{src_basename});
48604799
4861 const digest = if (try man.hit()) man.final() else blk: {4800 const digest = if (try man.hit()) man.final() else blk: {
...@@ -4867,17 +4806,12 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32...@@ -4867,17 +4806,12 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
4867 var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{});4806 var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{});
4868 defer o_dir.close();4807 defer o_dir.close();
48694808
4870 var output_file = o_dir.createFile(res_basename, .{}) catch |err| {4809 const in_rc_path = try comp.local_cache_directory.join(comp.gpa, &.{
4871 const output_file_path = try comp.local_cache_directory.join(arena, &.{ o_sub_path, res_basename });4810 o_sub_path, rc_basename,
4872 return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ output_file_path, @errorName(err) });4811 });
4873 };4812 const out_res_path = try comp.local_cache_directory.join(comp.gpa, &.{
4874 var output_file_closed = false;4813 o_sub_path, res_basename,
4875 defer if (!output_file_closed) output_file.close();4814 });
4876
4877 var diagnostics = resinator.errors.Diagnostics.init(arena);
4878 defer diagnostics.deinit();
4879
4880 var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
48814815
4882 // In .rc files, a " within a quoted string is escaped as ""4816 // In .rc files, a " within a quoted string is escaped as ""
4883 const fmtRcEscape = struct {4817 const fmtRcEscape = struct {
...@@ -4899,28 +4833,47 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32...@@ -4899,28 +4833,47 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
4899 // 1 is CREATEPROCESS_MANIFEST_RESOURCE_ID which is the default ID used for RT_MANIFEST resources4833 // 1 is CREATEPROCESS_MANIFEST_RESOURCE_ID which is the default ID used for RT_MANIFEST resources
4900 // 24 is RT_MANIFEST4834 // 24 is RT_MANIFEST
4901 const input = try std.fmt.allocPrint(arena, "1 24 \"{s}\"", .{fmtRcEscape(src_path)});4835 const input = try std.fmt.allocPrint(arena, "1 24 \"{s}\"", .{fmtRcEscape(src_path)});
4836 try o_dir.writeFile(rc_basename, input);
4837
4838 var argv = std.ArrayList([]const u8).init(comp.gpa);
4839 defer argv.deinit();
4840
4841 try argv.appendSlice(&.{
4842 self_exe_path,
4843 "rc",
4844 "/:no-preprocess",
4845 "/x", // ignore INCLUDE environment variable
4846 "/c65001", // UTF-8 codepage
4847 "/:auto-includes",
4848 "none",
4849 });
4850 try argv.appendSlice(&.{ "--", in_rc_path, out_res_path });
49024851
4903 resinator.compile.compile(arena, input, output_buffered_stream.writer(), .{4852 var child = std.ChildProcess.init(argv.items, arena);
4904 .cwd = std.fs.cwd(),4853 child.stdin_behavior = .Ignore;
4905 .diagnostics = &diagnostics,4854 child.stdout_behavior = .Ignore;
4906 .ignore_include_env_var = true,4855 child.stderr_behavior = .Pipe;
4907 .default_code_page = .utf8,4856
4908 }) catch |err| switch (err) {4857 try child.spawn();
4909 error.ParseError, error.CompileError => {4858
4910 // Delete the output file on error4859 const stderr_reader = child.stderr.?.reader();
4911 output_file.close();4860 const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
4912 output_file_closed = true;4861 const term = child.wait() catch |err| {
4913 // Failing to delete is not really a big deal, so swallow any errors4862 return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
4914 o_dir.deleteFile(res_basename) catch {
4915 const output_file_path = try comp.local_cache_directory.join(arena, &.{ o_sub_path, res_basename });
4916 log.warn("failed to delete '{s}': {s}", .{ output_file_path, @errorName(err) });
4917 };
4918 return comp.failWin32ResourceCompile(win32_resource, input, &diagnostics, null);
4919 },
4920 else => |e| return e,
4921 };4863 };
49224864
4923 try output_buffered_stream.flush();4865 switch (term) {
4866 .Exited => |code| {
4867 if (code != 0) {
4868 log.err("zig rc failed with stderr:\n{s}", .{stderr});
4869 return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
4870 }
4871 },
4872 else => {
4873 log.err("zig rc terminated with stderr:\n{s}", .{stderr});
4874 return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
4875 },
4876 }
49244877
4925 break :blk digest;4878 break :blk digest;
4926 };4879 };
...@@ -4951,9 +4904,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32...@@ -4951,9 +4904,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
4951 const rc_basename_noext = src_basename[0 .. src_basename.len - std.fs.path.extension(src_basename).len];4904 const rc_basename_noext = src_basename[0 .. src_basename.len - std.fs.path.extension(src_basename).len];
49524905
4953 const digest = if (try man.hit()) man.final() else blk: {4906 const digest = if (try man.hit()) man.final() else blk: {
4954 const rcpp_filename = try std.fmt.allocPrint(arena, "{s}.rcpp", .{rc_basename_noext});
4955
4956 const out_rcpp_path = try comp.tmpFilePath(arena, rcpp_filename);
4957 var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{});4907 var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{});
4958 defer zig_cache_tmp_dir.close();4908 defer zig_cache_tmp_dir.close();
49594909
...@@ -4963,193 +4913,89 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32...@@ -4963,193 +4913,89 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
4963 // so we need a temporary filename.4913 // so we need a temporary filename.
4964 const out_res_path = try comp.tmpFilePath(arena, res_filename);4914 const out_res_path = try comp.tmpFilePath(arena, res_filename);
49654915
4966 var options = options: {
4967 var resinator_args = try std.ArrayListUnmanaged([]const u8).initCapacity(comp.gpa, rc_src.extra_flags.len + 4);
4968 defer resinator_args.deinit(comp.gpa);
4969
4970 resinator_args.appendAssumeCapacity(""); // dummy 'process name' arg
4971 resinator_args.appendSliceAssumeCapacity(rc_src.extra_flags);
4972 resinator_args.appendSliceAssumeCapacity(&.{ "--", out_rcpp_path, out_res_path });
4973
4974 var cli_diagnostics = resinator.cli.Diagnostics.init(comp.gpa);
4975 defer cli_diagnostics.deinit();
4976 const options = resinator.cli.parse(comp.gpa, resinator_args.items, &cli_diagnostics) catch |err| switch (err) {
4977 error.ParseError => {
4978 return comp.failWin32ResourceCli(win32_resource, &cli_diagnostics);
4979 },
4980 else => |e| return e,
4981 };
4982 break :options options;
4983 };
4984 defer options.deinit();
4985
4986 // We never want to read the INCLUDE environment variable, so
4987 // unconditionally set `ignore_include_env_var` to true
4988 options.ignore_include_env_var = true;
4989
4990 if (options.preprocess != .yes) {
4991 return comp.failWin32Resource(win32_resource, "the '{s}' option is not supported in this context", .{switch (options.preprocess) {
4992 .no => "/:no-preprocess",
4993 .only => "/p",
4994 .yes => unreachable,
4995 }});
4996 }
4997
4998 var argv = std.ArrayList([]const u8).init(comp.gpa);4916 var argv = std.ArrayList([]const u8).init(comp.gpa);
4999 defer argv.deinit();4917 defer argv.deinit();
50004918
5001 try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });4919 const depfile_filename = try std.fmt.allocPrint(arena, "{s}.d.json", .{rc_basename_noext});
50024920 const out_dep_path = try comp.tmpFilePath(arena, depfile_filename);
5003 try resinator.preprocess.appendClangArgs(arena, &argv, options, .{4921 try argv.appendSlice(&.{
5004 .clang_target = null, // handled by addCCArgs4922 self_exe_path,
5005 .system_include_paths = &.{}, // handled by addCCArgs4923 "rc",
5006 .needs_gnu_workaround = comp.getTarget().isGnu(),4924 "/:depfile",
5007 .nostdinc = false, // handled by addCCArgs4925 out_dep_path,
4926 "/:depfile-fmt",
4927 "json",
4928 "/x", // ignore INCLUDE environment variable
4929 "/:auto-includes",
4930 @tagName(comp.rc_includes),
5008 });4931 });
50094932 // While these defines are not normally present when calling rc.exe directly,
5010 try argv.append(rc_src.src_path);
5011 try argv.appendSlice(&[_][]const u8{
5012 "-o",
5013 out_rcpp_path,
5014 });
5015
5016 const out_dep_path = try std.fmt.allocPrint(arena, "{s}.d", .{out_rcpp_path});
5017 // Note: addCCArgs will implicitly add _DEBUG/NDEBUG depending on the optimization
5018 // mode. While these defines are not normally present when calling rc.exe directly,
5019 // them being defined matches the behavior of how MSVC calls rc.exe which is the more4933 // them being defined matches the behavior of how MSVC calls rc.exe which is the more
5020 // relevant behavior in this case.4934 // relevant behavior in this case.
5021 try comp.addCCArgs(arena, &argv, .rc, out_dep_path, rc_src.owner);4935 switch (rc_src.owner.optimize_mode) {
50224936 .Debug => try argv.append("-D_DEBUG"),
5023 if (comp.verbose_cc) {4937 .ReleaseSafe => {},
5024 dump_argv(argv.items);4938 .ReleaseFast, .ReleaseSmall => try argv.append("-DNDEBUG"),
5025 }4939 }
4940 try argv.appendSlice(rc_src.extra_flags);
4941 try argv.appendSlice(&.{ "--", rc_src.src_path, out_res_path });
50264942
5027 if (std.process.can_spawn) {4943 var child = std.ChildProcess.init(argv.items, arena);
5028 var child = std.ChildProcess.init(argv.items, arena);4944 child.stdin_behavior = .Ignore;
5029 child.stdin_behavior = .Ignore;4945 child.stdout_behavior = .Ignore;
5030 child.stdout_behavior = .Ignore;4946 child.stderr_behavior = .Pipe;
5031 child.stderr_behavior = .Pipe;
5032
5033 try child.spawn();
50344947
5035 const stderr_reader = child.stderr.?.reader();4948 try child.spawn();
5036
5037 const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
5038
5039 const term = child.wait() catch |err| {
5040 return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
5041 };
5042
5043 switch (term) {
5044 .Exited => |code| {
5045 if (code != 0) {
5046 // TODO parse clang stderr and turn it into an error message
5047 // and then call failCObjWithOwnedErrorMsg
5048 log.err("clang preprocessor failed with stderr:\n{s}", .{stderr});
5049 return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{code});
5050 }
5051 },
5052 else => {
5053 log.err("clang preprocessor terminated with stderr:\n{s}", .{stderr});
5054 return comp.failWin32Resource(win32_resource, "clang preprocessor terminated unexpectedly", .{});
5055 },
5056 }
5057 } else {
5058 const exit_code = try clangMain(arena, argv.items);
5059 if (exit_code != 0) {
5060 return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{exit_code});
5061 }
5062 }
50634949
5064 const dep_basename = std.fs.path.basename(out_dep_path);4950 const stderr_reader = child.stderr.?.reader();
5065 // Add the files depended on to the cache system.4951 const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
5066 try man.addDepFilePost(zig_cache_tmp_dir, dep_basename);4952 const term = child.wait() catch |err| {
5067 switch (comp.cache_use) {4953 return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
5068 .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| {
5069 whole.cache_manifest_mutex.lock();
5070 defer whole.cache_manifest_mutex.unlock();
5071 try whole_cache_manifest.addDepFilePost(zig_cache_tmp_dir, dep_basename);
5072 },
5073 .incremental => {},
5074 }
5075 // Just to save disk space, we delete the file because it is never needed again.
5076 zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| {
5077 log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) });
5078 };4954 };
50794955
5080 const full_input = std.fs.cwd().readFileAlloc(arena, out_rcpp_path, std.math.maxInt(usize)) catch |err| switch (err) {4956 switch (term) {
5081 error.OutOfMemory => return error.OutOfMemory,4957 .Exited => |code| {
5082 else => |e| {4958 if (code != 0) {
5083 return comp.failWin32Resource(win32_resource, "failed to read preprocessed file '{s}': {s}", .{ out_rcpp_path, @errorName(e) });4959 log.err("zig rc failed with stderr:\n{s}", .{stderr});
4960 return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
4961 }
4962 },
4963 else => {
4964 log.err("zig rc terminated with stderr:\n{s}", .{stderr});
4965 return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
5084 },4966 },
5085 };
5086
5087 var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(arena, full_input, full_input, .{ .initial_filename = rc_src.src_path });
5088 defer mapping_results.mappings.deinit(arena);
5089
5090 const final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
5091
5092 var output_file = zig_cache_tmp_dir.createFile(out_res_path, .{}) catch |err| {
5093 return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ out_res_path, @errorName(err) });
5094 };
5095 var output_file_closed = false;
5096 defer if (!output_file_closed) output_file.close();
5097
5098 var diagnostics = resinator.errors.Diagnostics.init(arena);
5099 defer diagnostics.deinit();
5100
5101 var dependencies_list = std.ArrayList([]const u8).init(comp.gpa);
5102 defer {
5103 for (dependencies_list.items) |item| {
5104 comp.gpa.free(item);
5105 }
5106 dependencies_list.deinit();
5107 }4967 }
51084968
5109 var output_buffered_stream = std.io.bufferedWriter(output_file.writer());4969 // Read depfile and update cache manifest
51104970 {
5111 resinator.compile.compile(arena, final_input, output_buffered_stream.writer(), .{4971 const dep_basename = std.fs.path.basename(out_dep_path);
5112 .cwd = std.fs.cwd(),4972 const dep_file_contents = try zig_cache_tmp_dir.readFileAlloc(arena, dep_basename, 50 * 1024 * 1024);
5113 .diagnostics = &diagnostics,4973 defer arena.free(dep_file_contents);
5114 .source_mappings = &mapping_results.mappings,
5115 .dependencies_list = &dependencies_list,
5116 .system_include_paths = comp.rc_include_dir_list,
5117 .ignore_include_env_var = true,
5118 // options
5119 .extra_include_paths = options.extra_include_paths.items,
5120 .default_language_id = options.default_language_id,
5121 .default_code_page = options.default_code_page orelse .windows1252,
5122 .verbose = options.verbose,
5123 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
5124 .max_string_literal_codepoints = options.max_string_literal_codepoints,
5125 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
5126 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
5127 }) catch |err| switch (err) {
5128 error.ParseError, error.CompileError => {
5129 // Delete the output file on error
5130 output_file.close();
5131 output_file_closed = true;
5132 // Failing to delete is not really a big deal, so swallow any errors
5133 zig_cache_tmp_dir.deleteFile(out_res_path) catch {
5134 log.warn("failed to delete '{s}': {s}", .{ out_res_path, @errorName(err) });
5135 };
5136 return comp.failWin32ResourceCompile(win32_resource, final_input, &diagnostics, mapping_results.mappings);
5137 },
5138 else => |e| return e,
5139 };
51404974
5141 try output_buffered_stream.flush();4975 const value = try std.json.parseFromSliceLeaky(std.json.Value, arena, dep_file_contents, .{});
4976 if (value != .array) {
4977 return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{});
4978 }
51424979
5143 for (dependencies_list.items) |dep_file_path| {4980 for (value.array.items) |element| {
5144 try man.addFilePost(dep_file_path);4981 if (element != .string) {
5145 switch (comp.cache_use) {4982 return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{});
5146 .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| {4983 }
5147 whole.cache_manifest_mutex.lock();4984 const dep_file_path = element.string;
5148 defer whole.cache_manifest_mutex.unlock();4985 try man.addFilePost(dep_file_path);
5149 try whole_cache_manifest.addFilePost(dep_file_path);4986 switch (comp.cache_use) {
5150 },4987 .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| {
5151 .incremental => {},4988 whole.cache_manifest_mutex.lock();
4989 defer whole.cache_manifest_mutex.unlock();
4990 try whole_cache_manifest.addFilePost(dep_file_path);
4991 },
4992 .incremental => {},
4993 }
5152 }4994 }
4995 // Just to save disk space, we delete the file because it is never needed again.
4996 zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| {
4997 log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) });
4998 };
5153 }4999 }
51545000
5155 // Rename into place.5001 // Rename into place.
...@@ -5159,8 +5005,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32...@@ -5159,8 +5005,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
5159 defer o_dir.close();5005 defer o_dir.close();
5160 const tmp_basename = std.fs.path.basename(out_res_path);5006 const tmp_basename = std.fs.path.basename(out_res_path);
5161 try std.fs.rename(zig_cache_tmp_dir, tmp_basename, o_dir, res_filename);5007 try std.fs.rename(zig_cache_tmp_dir, tmp_basename, o_dir, res_filename);
5162 const tmp_rcpp_basename = std.fs.path.basename(out_rcpp_path);
5163 try std.fs.rename(zig_cache_tmp_dir, tmp_rcpp_basename, o_dir, rcpp_filename);
5164 break :blk digest;5008 break :blk digest;
5165 };5009 };
51665010
...@@ -5352,16 +5196,9 @@ pub fn addCCArgs(...@@ -5352,16 +5196,9 @@ pub fn addCCArgs(
5352 try argv.append("-isystem");5196 try argv.append("-isystem");
5353 try argv.append(c_headers_dir);5197 try argv.append(c_headers_dir);
53545198
5355 if (ext == .rc) {5199 for (comp.libc_include_dir_list) |include_dir| {
5356 for (comp.rc_include_dir_list) |include_dir| {5200 try argv.append("-isystem");
5357 try argv.append("-isystem");5201 try argv.append(include_dir);
5358 try argv.append(include_dir);
5359 }
5360 } else {
5361 for (comp.libc_include_dir_list) |include_dir| {
5362 try argv.append("-isystem");
5363 try argv.append(include_dir);
5364 }
5365 }5202 }
53665203
5367 if (target.cpu.model.llvm_name) |llvm_name| {5204 if (target.cpu.model.llvm_name) |llvm_name| {
...@@ -5726,167 +5563,6 @@ fn failWin32ResourceWithOwnedBundle(...@@ -5726,167 +5563,6 @@ fn failWin32ResourceWithOwnedBundle(
5726 return error.AnalysisFail;5563 return error.AnalysisFail;
5727}5564}
57285565
5729fn failWin32ResourceCli(
5730 comp: *Compilation,
5731 win32_resource: *Win32Resource,
5732 diagnostics: *resinator.cli.Diagnostics,
5733) SemaError {
5734 @setCold(true);
5735
5736 var bundle: ErrorBundle.Wip = undefined;
5737 try bundle.init(comp.gpa);
5738 errdefer bundle.deinit();
5739
5740 try bundle.addRootErrorMessage(.{
5741 .msg = try bundle.addString("invalid command line option(s)"),
5742 .src_loc = try bundle.addSourceLocation(.{
5743 .src_path = try bundle.addString(switch (win32_resource.src) {
5744 .rc => |rc_src| rc_src.src_path,
5745 .manifest => |manifest_src| manifest_src,
5746 }),
5747 .line = 0,
5748 .column = 0,
5749 .span_start = 0,
5750 .span_main = 0,
5751 .span_end = 0,
5752 }),
5753 });
5754
5755 var cur_err: ?ErrorBundle.ErrorMessage = null;
5756 var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
5757 defer cur_notes.deinit(comp.gpa);
5758 for (diagnostics.errors.items) |err_details| {
5759 switch (err_details.type) {
5760 .err => {
5761 if (cur_err) |err| {
5762 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5763 }
5764 cur_err = .{
5765 .msg = try bundle.addString(err_details.msg.items),
5766 };
5767 cur_notes.clearRetainingCapacity();
5768 },
5769 .warning => cur_err = null,
5770 .note => {
5771 if (cur_err == null) continue;
5772 cur_err.?.notes_len += 1;
5773 try cur_notes.append(comp.gpa, .{
5774 .msg = try bundle.addString(err_details.msg.items),
5775 });
5776 },
5777 }
5778 }
5779 if (cur_err) |err| {
5780 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5781 }
5782
5783 const finished_bundle = try bundle.toOwnedBundle("");
5784 return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle);
5785}
5786
5787fn failWin32ResourceCompile(
5788 comp: *Compilation,
5789 win32_resource: *Win32Resource,
5790 source: []const u8,
5791 diagnostics: *resinator.errors.Diagnostics,
5792 opt_mappings: ?resinator.source_mapping.SourceMappings,
5793) SemaError {
5794 @setCold(true);
5795
5796 var bundle: ErrorBundle.Wip = undefined;
5797 try bundle.init(comp.gpa);
5798 errdefer bundle.deinit();
5799
5800 var msg_buf: std.ArrayListUnmanaged(u8) = .{};
5801 defer msg_buf.deinit(comp.gpa);
5802 var cur_err: ?ErrorBundle.ErrorMessage = null;
5803 var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
5804 defer cur_notes.deinit(comp.gpa);
5805 for (diagnostics.errors.items) |err_details| {
5806 switch (err_details.type) {
5807 .hint => continue,
5808 // Clear the current error so that notes don't bleed into unassociated errors
5809 .warning => {
5810 cur_err = null;
5811 continue;
5812 },
5813 .note => if (cur_err == null) continue,
5814 .err => {},
5815 }
5816 const err_line, const err_filename = blk: {
5817 if (opt_mappings) |mappings| {
5818 const corresponding_span = mappings.get(err_details.token.line_number);
5819 const corresponding_file = mappings.files.get(corresponding_span.filename_offset);
5820 const err_line = corresponding_span.start_line;
5821 break :blk .{ err_line, corresponding_file };
5822 } else {
5823 break :blk .{ err_details.token.line_number, "<generated rc>" };
5824 }
5825 };
5826
5827 const source_line_start = err_details.token.getLineStart(source);
5828 const column = err_details.token.calculateColumn(source, 1, source_line_start);
5829
5830 msg_buf.clearRetainingCapacity();
5831 try err_details.render(msg_buf.writer(comp.gpa), source, diagnostics.strings.items);
5832
5833 const src_loc = src_loc: {
5834 var src_loc: ErrorBundle.SourceLocation = .{
5835 .src_path = try bundle.addString(err_filename),
5836 .line = @intCast(err_line - 1), // 1-based -> 0-based
5837 .column = @intCast(column),
5838 .span_start = 0,
5839 .span_main = 0,
5840 .span_end = 0,
5841 };
5842 if (err_details.print_source_line) {
5843 const source_line = err_details.token.getLine(source, source_line_start);
5844 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
5845 src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len);
5846 src_loc.span_main = @intCast(visual_info.point_offset);
5847 src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len);
5848 src_loc.source_line = try bundle.addString(source_line);
5849 }
5850 break :src_loc try bundle.addSourceLocation(src_loc);
5851 };
5852
5853 switch (err_details.type) {
5854 .err => {
5855 if (cur_err) |err| {
5856 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5857 }
5858 cur_err = .{
5859 .msg = try bundle.addString(msg_buf.items),
5860 .src_loc = src_loc,
5861 };
5862 cur_notes.clearRetainingCapacity();
5863 },
5864 .note => {
5865 cur_err.?.notes_len += 1;
5866 try cur_notes.append(comp.gpa, .{
5867 .msg = try bundle.addString(msg_buf.items),
5868 .src_loc = src_loc,
5869 });
5870 },
5871 .warning, .hint => unreachable,
5872 }
5873 }
5874 if (cur_err) |err| {
5875 try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
5876 }
5877
5878 const finished_bundle = try bundle.toOwnedBundle("");
5879 return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle);
5880}
5881
5882fn win32ResourceFlushErrorMessage(wip: *ErrorBundle.Wip, msg: ErrorBundle.ErrorMessage, notes: []const ErrorBundle.ErrorMessage) !void {
5883 try wip.addRootErrorMessage(msg);
5884 const notes_start = try wip.reserveNotes(@intCast(notes.len));
5885 for (notes_start.., notes) |i, note| {
5886 wip.extra.items[i] = @intFromEnum(wip.addErrorMessageAssumeCapacity(note));
5887 }
5888}
5889
5890pub const FileExt = enum {5566pub const FileExt = enum {
5891 c,5567 c,
5892 cpp,5568 cpp,
src/main.zig+6-271
...@@ -291,7 +291,12 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {...@@ -291,7 +291,12 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
291 } else if (mem.eql(u8, cmd, "translate-c")) {291 } else if (mem.eql(u8, cmd, "translate-c")) {
292 return buildOutputType(gpa, arena, args, .translate_c);292 return buildOutputType(gpa, arena, args, .translate_c);
293 } else if (mem.eql(u8, cmd, "rc")) {293 } else if (mem.eql(u8, cmd, "rc")) {
294 return cmdRc(gpa, arena, args[1..]);294 return jitCmd(gpa, arena, cmd_args, .{
295 .cmd_name = "resinator",
296 .root_src_path = "resinator/main.zig",
297 .depend_on_aro = true,
298 .prepend_zig_lib_dir_path = true,
299 });
295 } else if (mem.eql(u8, cmd, "fmt")) {300 } else if (mem.eql(u8, cmd, "fmt")) {
296 return jitCmd(gpa, arena, cmd_args, .{301 return jitCmd(gpa, arena, cmd_args, .{
297 .cmd_name = "fmt",302 .cmd_name = "fmt",
...@@ -4625,276 +4630,6 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati...@@ -4625,276 +4630,6 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati
4625 }4630 }
4626}4631}
46274632
4628fn cmdRc(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
4629 const resinator = @import("resinator.zig");
4630
4631 const stderr = std.io.getStdErr();
4632 const stderr_config = std.io.tty.detectConfig(stderr);
4633
4634 var options = options: {
4635 var cli_diagnostics = resinator.cli.Diagnostics.init(gpa);
4636 defer cli_diagnostics.deinit();
4637 var options = resinator.cli.parse(gpa, args, &cli_diagnostics) catch |err| switch (err) {
4638 error.ParseError => {
4639 cli_diagnostics.renderToStdErr(args, stderr_config);
4640 process.exit(1);
4641 },
4642 else => |e| return e,
4643 };
4644 try options.maybeAppendRC(std.fs.cwd());
4645
4646 // print any warnings/notes
4647 cli_diagnostics.renderToStdErr(args, stderr_config);
4648 // If there was something printed, then add an extra newline separator
4649 // so that there is a clear separation between the cli diagnostics and whatever
4650 // gets printed after
4651 if (cli_diagnostics.errors.items.len > 0) {
4652 std.debug.print("\n", .{});
4653 }
4654 break :options options;
4655 };
4656 defer options.deinit();
4657
4658 if (options.print_help_and_exit) {
4659 try resinator.cli.writeUsage(stderr.writer(), "zig rc");
4660 return;
4661 }
4662
4663 const stdout_writer = std.io.getStdOut().writer();
4664 if (options.verbose) {
4665 try options.dumpVerbose(stdout_writer);
4666 try stdout_writer.writeByte('\n');
4667 }
4668
4669 const full_input = full_input: {
4670 if (options.preprocess != .no) {
4671 if (!build_options.have_llvm) {
4672 fatal("clang not available: compiler built without LLVM extensions", .{});
4673 }
4674
4675 var argv = std.ArrayList([]const u8).init(gpa);
4676 defer argv.deinit();
4677
4678 const self_exe_path = try introspect.findZigExePath(arena);
4679 var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
4680 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to find zig installation directory: {s}", .{@errorName(err)});
4681 process.exit(1);
4682 };
4683 defer zig_lib_directory.handle.close();
4684
4685 const include_args = detectRcIncludeDirs(arena, zig_lib_directory.path.?, options.auto_includes) catch |err| {
4686 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to detect system include directories: {s}", .{@errorName(err)});
4687 process.exit(1);
4688 };
4689
4690 try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });
4691
4692 const clang_target = clang_target: {
4693 if (include_args.target_abi) |abi| {
4694 break :clang_target try std.fmt.allocPrint(arena, "x86_64-unknown-windows-{s}", .{abi});
4695 }
4696 break :clang_target "x86_64-unknown-windows";
4697 };
4698 try resinator.preprocess.appendClangArgs(arena, &argv, options, .{
4699 .clang_target = clang_target,
4700 .system_include_paths = include_args.include_paths,
4701 .needs_gnu_workaround = if (include_args.target_abi) |abi| std.mem.eql(u8, abi, "gnu") else false,
4702 .nostdinc = true,
4703 });
4704
4705 try argv.append(options.input_filename);
4706
4707 if (options.verbose) {
4708 try stdout_writer.writeAll("Preprocessor: zig clang\n");
4709 for (argv.items[0 .. argv.items.len - 1]) |arg| {
4710 try stdout_writer.print("{s} ", .{arg});
4711 }
4712 try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]});
4713 }
4714
4715 if (process.can_spawn) {
4716 const result = std.ChildProcess.run(.{
4717 .allocator = gpa,
4718 .argv = argv.items,
4719 .max_output_bytes = std.math.maxInt(u32),
4720 }) catch |err| {
4721 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to spawn preprocessor child process: {s}", .{@errorName(err)});
4722 process.exit(1);
4723 };
4724 errdefer gpa.free(result.stdout);
4725 defer gpa.free(result.stderr);
4726
4727 switch (result.term) {
4728 .Exited => |code| {
4729 if (code != 0) {
4730 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{code});
4731 try stderr.writeAll(result.stderr);
4732 try stderr.writeAll("\n");
4733 process.exit(1);
4734 }
4735 },
4736 .Signal, .Stopped, .Unknown => {
4737 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor terminated unexpectedly ({s}):", .{@tagName(result.term)});
4738 try stderr.writeAll(result.stderr);
4739 try stderr.writeAll("\n");
4740 process.exit(1);
4741 },
4742 }
4743
4744 break :full_input result.stdout;
4745 } else {
4746 // need to use an intermediate file
4747 const rand_int = std.crypto.random.int(u64);
4748 const preprocessed_path = try std.fmt.allocPrint(gpa, "resinator{x}.rcpp", .{rand_int});
4749 defer gpa.free(preprocessed_path);
4750 defer std.fs.cwd().deleteFile(preprocessed_path) catch {};
4751
4752 try argv.appendSlice(&.{ "-o", preprocessed_path });
4753 const exit_code = try clangMain(arena, argv.items);
4754 if (exit_code != 0) {
4755 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{exit_code});
4756 process.exit(1);
4757 }
4758 break :full_input std.fs.cwd().readFileAlloc(gpa, preprocessed_path, std.math.maxInt(usize)) catch |err| {
4759 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read preprocessed file path '{s}': {s}", .{ preprocessed_path, @errorName(err) });
4760 process.exit(1);
4761 };
4762 }
4763 } else {
4764 break :full_input std.fs.cwd().readFileAlloc(gpa, options.input_filename, std.math.maxInt(usize)) catch |err| {
4765 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
4766 process.exit(1);
4767 };
4768 }
4769 };
4770 defer gpa.free(full_input);
4771
4772 if (options.preprocess == .only) {
4773 std.fs.cwd().writeFile(options.output_filename, full_input) catch |err| {
4774 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to write output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
4775 process.exit(1);
4776 };
4777 return cleanExit();
4778 }
4779
4780 var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(gpa, full_input, full_input, .{ .initial_filename = options.input_filename });
4781 defer mapping_results.mappings.deinit(gpa);
4782
4783 const final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
4784
4785 var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
4786 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
4787 process.exit(1);
4788 };
4789 var output_file_closed = false;
4790 defer if (!output_file_closed) output_file.close();
4791
4792 var diagnostics = resinator.errors.Diagnostics.init(gpa);
4793 defer diagnostics.deinit();
4794
4795 var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
4796
4797 resinator.compile.compile(gpa, final_input, output_buffered_stream.writer(), .{
4798 .cwd = std.fs.cwd(),
4799 .diagnostics = &diagnostics,
4800 .source_mappings = &mapping_results.mappings,
4801 .dependencies_list = null,
4802 .ignore_include_env_var = options.ignore_include_env_var,
4803 .extra_include_paths = options.extra_include_paths.items,
4804 .default_language_id = options.default_language_id,
4805 .default_code_page = options.default_code_page orelse .windows1252,
4806 .verbose = options.verbose,
4807 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
4808 .max_string_literal_codepoints = options.max_string_literal_codepoints,
4809 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
4810 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
4811 }) catch |err| switch (err) {
4812 error.ParseError, error.CompileError => {
4813 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
4814 // Delete the output file on error
4815 output_file.close();
4816 output_file_closed = true;
4817 // Failing to delete is not really a big deal, so swallow any errors
4818 std.fs.cwd().deleteFile(options.output_filename) catch {};
4819 process.exit(1);
4820 },
4821 else => |e| return e,
4822 };
4823
4824 try output_buffered_stream.flush();
4825
4826 // print any warnings/notes
4827 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
4828
4829 return cleanExit();
4830}
4831
4832const RcIncludeArgs = struct {
4833 include_paths: []const []const u8 = &.{},
4834 target_abi: ?[]const u8 = null,
4835};
4836
4837fn detectRcIncludeDirs(arena: Allocator, zig_lib_dir: []const u8, auto_includes: @import("resinator.zig").cli.Options.AutoIncludes) !RcIncludeArgs {
4838 if (auto_includes == .none) return .{};
4839 var cur_includes = auto_includes;
4840 if (builtin.target.os.tag != .windows) {
4841 switch (cur_includes) {
4842 // MSVC can't be found when the host isn't Windows, so short-circuit.
4843 .msvc => return error.WindowsSdkNotFound,
4844 // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts.
4845 .any => cur_includes = .gnu,
4846 .gnu => {},
4847 .none => unreachable,
4848 }
4849 }
4850 while (true) {
4851 switch (cur_includes) {
4852 .any, .msvc => {
4853 const target_query: std.Target.Query = .{
4854 .os_tag = .windows,
4855 .abi = .msvc,
4856 };
4857 const target = std.zig.resolveTargetQueryOrFatal(target_query);
4858 const is_native_abi = target_query.isNativeAbi();
4859 const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| {
4860 if (cur_includes == .any) {
4861 // fall back to mingw
4862 cur_includes = .gnu;
4863 continue;
4864 }
4865 return err;
4866 };
4867 if (detected_libc.libc_include_dir_list.len == 0) {
4868 if (cur_includes == .any) {
4869 // fall back to mingw
4870 cur_includes = .gnu;
4871 continue;
4872 }
4873 return error.WindowsSdkNotFound;
4874 }
4875 return .{
4876 .include_paths = detected_libc.libc_include_dir_list,
4877 .target_abi = "msvc",
4878 };
4879 },
4880 .gnu => {
4881 const target_query: std.Target.Query = .{
4882 .os_tag = .windows,
4883 .abi = .gnu,
4884 };
4885 const target = std.zig.resolveTargetQueryOrFatal(target_query);
4886 const is_native_abi = target_query.isNativeAbi();
4887 const detected_libc = try std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null);
4888 return .{
4889 .include_paths = detected_libc.libc_include_dir_list,
4890 .target_abi = "gnu",
4891 };
4892 },
4893 .none => unreachable,
4894 }
4895 }
4896}
4897
4898const usage_init =4633const usage_init =
4899 \\Usage: zig init4634 \\Usage: zig init
4900 \\4635 \\
src/resinator.zig deleted-25
...@@ -1,25 +0,0 @@
1comptime {
2 if (@import("build_options").only_core_functionality) {
3 @compileError("resinator included in only_core_functionality build");
4 }
5}
6
7pub const ani = @import("resinator/ani.zig");
8pub const ast = @import("resinator/ast.zig");
9pub const bmp = @import("resinator/bmp.zig");
10pub const cli = @import("resinator/cli.zig");
11pub const code_pages = @import("resinator/code_pages.zig");
12pub const comments = @import("resinator/comments.zig");
13pub const compile = @import("resinator/compile.zig");
14pub const errors = @import("resinator/errors.zig");
15pub const ico = @import("resinator/ico.zig");
16pub const lang = @import("resinator/lang.zig");
17pub const lex = @import("resinator/lex.zig");
18pub const literals = @import("resinator/literals.zig");
19pub const parse = @import("resinator/parse.zig");
20pub const preprocess = @import("resinator/preprocess.zig");
21pub const rc = @import("resinator/rc.zig");
22pub const res = @import("resinator/res.zig");
23pub const source_mapping = @import("resinator/source_mapping.zig");
24pub const utils = @import("resinator/utils.zig");
25pub const windows1252 = @import("resinator/windows1252.zig");
src/resinator/ani.zig deleted-58
...@@ -1,58 +0,0 @@
1//! https://en.wikipedia.org/wiki/Resource_Interchange_File_Format
2//! https://www.moon-soft.com/program/format/windows/ani.htm
3//! https://www.gdgsoft.com/anituner/help/aniformat.htm
4//! https://www.lomont.org/software/aniexploit/ExploitANI.pdf
5//!
6//! RIFF( 'ACON'
7//! [LIST( 'INFO' <info_data> )]
8//! [<DISP_ck>]
9//! anih( <ani_header> )
10//! [rate( <rate_info> )]
11//! ['seq '( <sequence_info> )]
12//! LIST( 'fram' icon( <icon_file> ) ... )
13//! )
14
15const std = @import("std");
16
17const AF_ICON: u32 = 1;
18
19pub fn isAnimatedIcon(reader: anytype) bool {
20 const flags = getAniheaderFlags(reader) catch return false;
21 return flags & AF_ICON == AF_ICON;
22}
23
24fn getAniheaderFlags(reader: anytype) !u32 {
25 const riff_header = try reader.readBytesNoEof(4);
26 if (!std.mem.eql(u8, &riff_header, "RIFF")) return error.InvalidFormat;
27
28 _ = try reader.readInt(u32, .little); // 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.readInt(u32, .little);
36 if (!std.mem.eql(u8, &chunk_id, "anih")) {
37 // TODO: Move file cursor instead of skipBytes
38 try reader.skipBytes(chunk_len, .{});
39 continue;
40 }
41
42 const aniheader = try reader.readStruct(ANIHEADER);
43 return std.mem.nativeToLittle(u32, aniheader.flags);
44 }
45}
46
47/// From Microsoft Multimedia Data Standards Update April 15, 1994
48const ANIHEADER = extern struct {
49 cbSizeof: u32,
50 cFrames: u32,
51 cSteps: u32,
52 cx: u32,
53 cy: u32,
54 cBitCount: u32,
55 cPlanes: u32,
56 jifRate: u32,
57 flags: u32,
58};
src/resinator/ast.zig deleted-1084
...@@ -1,1084 +0,0 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const Token = @import("lex.zig").Token;
4const CodePage = @import("code_pages.zig").CodePage;
5
6pub const Tree = struct {
7 node: *Node,
8 input_code_pages: CodePageLookup,
9 output_code_pages: CodePageLookup,
10
11 /// not owned by the tree
12 source: []const u8,
13
14 arena: std.heap.ArenaAllocator.State,
15 allocator: Allocator,
16
17 pub fn deinit(self: *Tree) void {
18 self.arena.promote(self.allocator).deinit();
19 }
20
21 pub fn root(self: *Tree) *Node.Root {
22 return @fieldParentPtr(Node.Root, "base", self.node);
23 }
24
25 pub fn dump(self: *Tree, writer: anytype) @TypeOf(writer).Error!void {
26 try self.node.dump(self, writer, 0);
27 }
28};
29
30pub const CodePageLookup = struct {
31 lookup: std.ArrayListUnmanaged(CodePage) = .{},
32 allocator: Allocator,
33 default_code_page: CodePage,
34
35 pub fn init(allocator: Allocator, default_code_page: CodePage) CodePageLookup {
36 return .{
37 .allocator = allocator,
38 .default_code_page = default_code_page,
39 };
40 }
41
42 pub fn deinit(self: *CodePageLookup) void {
43 self.lookup.deinit(self.allocator);
44 }
45
46 /// line_num is 1-indexed
47 pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: CodePage) !void {
48 const index = line_num - 1;
49 if (index >= self.lookup.items.len) {
50 const new_size = line_num;
51 const missing_lines_start_index = self.lookup.items.len;
52 try self.lookup.resize(self.allocator, new_size);
53
54 // If there are any gaps created, we need to fill them in with the value of the
55 // last line before the gap. This can happen for e.g. string literals that
56 // span multiple lines, or if the start of a file has multiple empty lines.
57 const fill_value = if (missing_lines_start_index > 0)
58 self.lookup.items[missing_lines_start_index - 1]
59 else
60 self.default_code_page;
61 var i: usize = missing_lines_start_index;
62 while (i < new_size - 1) : (i += 1) {
63 self.lookup.items[i] = fill_value;
64 }
65 }
66 self.lookup.items[index] = code_page;
67 }
68
69 pub fn setForToken(self: *CodePageLookup, token: Token, code_page: CodePage) !void {
70 return self.setForLineNum(token.line_number, code_page);
71 }
72
73 /// line_num is 1-indexed
74 pub fn getForLineNum(self: CodePageLookup, line_num: usize) CodePage {
75 return self.lookup.items[line_num - 1];
76 }
77
78 pub fn getForToken(self: CodePageLookup, token: Token) CodePage {
79 return self.getForLineNum(token.line_number);
80 }
81};
82
83test "CodePageLookup" {
84 var lookup = CodePageLookup.init(std.testing.allocator, .windows1252);
85 defer lookup.deinit();
86
87 try lookup.setForLineNum(5, .utf8);
88 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));
89 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));
90 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));
91 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));
92 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));
93 try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len);
94
95 try lookup.setForLineNum(7, .windows1252);
96 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));
97 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));
98 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));
99 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));
100 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));
101 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(6));
102 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(7));
103 try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len);
104}
105
106pub const Node = struct {
107 id: Id,
108
109 pub const Id = enum {
110 root,
111 resource_external,
112 resource_raw_data,
113 literal,
114 binary_expression,
115 grouped_expression,
116 not_expression,
117 accelerators,
118 accelerator,
119 dialog,
120 control_statement,
121 toolbar,
122 menu,
123 menu_item,
124 menu_item_separator,
125 menu_item_ex,
126 popup,
127 popup_ex,
128 version_info,
129 version_statement,
130 block,
131 block_value,
132 block_value_value,
133 string_table,
134 string_table_string,
135 language_statement,
136 font_statement,
137 simple_statement,
138 invalid,
139
140 pub fn Type(comptime id: Id) type {
141 return switch (id) {
142 .root => Root,
143 .resource_external => ResourceExternal,
144 .resource_raw_data => ResourceRawData,
145 .literal => Literal,
146 .binary_expression => BinaryExpression,
147 .grouped_expression => GroupedExpression,
148 .not_expression => NotExpression,
149 .accelerators => Accelerators,
150 .accelerator => Accelerator,
151 .dialog => Dialog,
152 .control_statement => ControlStatement,
153 .toolbar => Toolbar,
154 .menu => Menu,
155 .menu_item => MenuItem,
156 .menu_item_separator => MenuItemSeparator,
157 .menu_item_ex => MenuItemEx,
158 .popup => Popup,
159 .popup_ex => PopupEx,
160 .version_info => VersionInfo,
161 .version_statement => VersionStatement,
162 .block => Block,
163 .block_value => BlockValue,
164 .block_value_value => BlockValueValue,
165 .string_table => StringTable,
166 .string_table_string => StringTableString,
167 .language_statement => LanguageStatement,
168 .font_statement => FontStatement,
169 .simple_statement => SimpleStatement,
170 .invalid => Invalid,
171 };
172 }
173 };
174
175 pub fn cast(base: *Node, comptime id: Id) ?*id.Type() {
176 if (base.id == id) {
177 return @fieldParentPtr(id.Type(), "base", base);
178 }
179 return null;
180 }
181
182 pub const Root = struct {
183 base: Node = .{ .id = .root },
184 body: []*Node,
185 };
186
187 pub const ResourceExternal = struct {
188 base: Node = .{ .id = .resource_external },
189 id: Token,
190 type: Token,
191 common_resource_attributes: []Token,
192 filename: *Node,
193 };
194
195 pub const ResourceRawData = struct {
196 base: Node = .{ .id = .resource_raw_data },
197 id: Token,
198 type: Token,
199 common_resource_attributes: []Token,
200 begin_token: Token,
201 raw_data: []*Node,
202 end_token: Token,
203 };
204
205 pub const Literal = struct {
206 base: Node = .{ .id = .literal },
207 token: Token,
208 };
209
210 pub const BinaryExpression = struct {
211 base: Node = .{ .id = .binary_expression },
212 operator: Token,
213 left: *Node,
214 right: *Node,
215 };
216
217 pub const GroupedExpression = struct {
218 base: Node = .{ .id = .grouped_expression },
219 open_token: Token,
220 expression: *Node,
221 close_token: Token,
222 };
223
224 pub const NotExpression = struct {
225 base: Node = .{ .id = .not_expression },
226 not_token: Token,
227 number_token: Token,
228 };
229
230 pub const Accelerators = struct {
231 base: Node = .{ .id = .accelerators },
232 id: Token,
233 type: Token,
234 common_resource_attributes: []Token,
235 optional_statements: []*Node,
236 begin_token: Token,
237 accelerators: []*Node,
238 end_token: Token,
239 };
240
241 pub const Accelerator = struct {
242 base: Node = .{ .id = .accelerator },
243 event: *Node,
244 idvalue: *Node,
245 type_and_options: []Token,
246 };
247
248 pub const Dialog = struct {
249 base: Node = .{ .id = .dialog },
250 id: Token,
251 type: Token,
252 common_resource_attributes: []Token,
253 x: *Node,
254 y: *Node,
255 width: *Node,
256 height: *Node,
257 help_id: ?*Node,
258 optional_statements: []*Node,
259 begin_token: Token,
260 controls: []*Node,
261 end_token: Token,
262 };
263
264 pub const ControlStatement = struct {
265 base: Node = .{ .id = .control_statement },
266 type: Token,
267 text: ?Token,
268 /// Only relevant for the user-defined CONTROL control
269 class: ?*Node,
270 id: *Node,
271 x: *Node,
272 y: *Node,
273 width: *Node,
274 height: *Node,
275 style: ?*Node,
276 exstyle: ?*Node,
277 help_id: ?*Node,
278 extra_data_begin: ?Token,
279 extra_data: []*Node,
280 extra_data_end: ?Token,
281
282 /// Returns true if this node describes a user-defined CONTROL control
283 /// https://learn.microsoft.com/en-us/windows/win32/menurc/control-control
284 pub fn isUserDefined(self: *const ControlStatement) bool {
285 return self.class != null;
286 }
287 };
288
289 pub const Toolbar = struct {
290 base: Node = .{ .id = .toolbar },
291 id: Token,
292 type: Token,
293 common_resource_attributes: []Token,
294 button_width: *Node,
295 button_height: *Node,
296 begin_token: Token,
297 /// Will contain Literal and SimpleStatement nodes
298 buttons: []*Node,
299 end_token: Token,
300 };
301
302 pub const Menu = struct {
303 base: Node = .{ .id = .menu },
304 id: Token,
305 type: Token,
306 common_resource_attributes: []Token,
307 optional_statements: []*Node,
308 /// `help_id` will never be non-null if `type` is MENU
309 help_id: ?*Node,
310 begin_token: Token,
311 items: []*Node,
312 end_token: Token,
313 };
314
315 pub const MenuItem = struct {
316 base: Node = .{ .id = .menu_item },
317 menuitem: Token,
318 text: Token,
319 result: *Node,
320 option_list: []Token,
321 };
322
323 pub const MenuItemSeparator = struct {
324 base: Node = .{ .id = .menu_item_separator },
325 menuitem: Token,
326 separator: Token,
327 };
328
329 pub const MenuItemEx = struct {
330 base: Node = .{ .id = .menu_item_ex },
331 menuitem: Token,
332 text: Token,
333 id: ?*Node,
334 type: ?*Node,
335 state: ?*Node,
336 };
337
338 pub const Popup = struct {
339 base: Node = .{ .id = .popup },
340 popup: Token,
341 text: Token,
342 option_list: []Token,
343 begin_token: Token,
344 items: []*Node,
345 end_token: Token,
346 };
347
348 pub const PopupEx = struct {
349 base: Node = .{ .id = .popup_ex },
350 popup: Token,
351 text: Token,
352 id: ?*Node,
353 type: ?*Node,
354 state: ?*Node,
355 help_id: ?*Node,
356 begin_token: Token,
357 items: []*Node,
358 end_token: Token,
359 };
360
361 pub const VersionInfo = struct {
362 base: Node = .{ .id = .version_info },
363 id: Token,
364 versioninfo: Token,
365 common_resource_attributes: []Token,
366 /// Will contain VersionStatement and/or SimpleStatement nodes
367 fixed_info: []*Node,
368 begin_token: Token,
369 block_statements: []*Node,
370 end_token: Token,
371 };
372
373 /// Used for FILEVERSION and PRODUCTVERSION statements
374 pub const VersionStatement = struct {
375 base: Node = .{ .id = .version_statement },
376 type: Token,
377 /// Between 1-4 parts
378 parts: []*Node,
379 };
380
381 pub const Block = struct {
382 base: Node = .{ .id = .block },
383 /// The BLOCK token itself
384 identifier: Token,
385 key: Token,
386 /// This is undocumented but BLOCK statements support values after
387 /// the key just like VALUE statements.
388 values: []*Node,
389 begin_token: Token,
390 children: []*Node,
391 end_token: Token,
392 };
393
394 pub const BlockValue = struct {
395 base: Node = .{ .id = .block_value },
396 /// The VALUE token itself
397 identifier: Token,
398 key: Token,
399 /// These will be BlockValueValue nodes
400 values: []*Node,
401 };
402
403 pub const BlockValueValue = struct {
404 base: Node = .{ .id = .block_value_value },
405 expression: *Node,
406 /// Whether or not the value has a trailing comma is relevant
407 trailing_comma: bool,
408 };
409
410 pub const StringTable = struct {
411 base: Node = .{ .id = .string_table },
412 type: Token,
413 common_resource_attributes: []Token,
414 optional_statements: []*Node,
415 begin_token: Token,
416 strings: []*Node,
417 end_token: Token,
418 };
419
420 pub const StringTableString = struct {
421 base: Node = .{ .id = .string_table_string },
422 id: *Node,
423 maybe_comma: ?Token,
424 string: Token,
425 };
426
427 pub const LanguageStatement = struct {
428 base: Node = .{ .id = .language_statement },
429 /// The LANGUAGE token itself
430 language_token: Token,
431 primary_language_id: *Node,
432 sublanguage_id: *Node,
433 };
434
435 pub const FontStatement = struct {
436 base: Node = .{ .id = .font_statement },
437 /// The FONT token itself
438 identifier: Token,
439 point_size: *Node,
440 typeface: Token,
441 weight: ?*Node,
442 italic: ?*Node,
443 char_set: ?*Node,
444 };
445
446 /// A statement with one value associated with it.
447 /// Used for CAPTION, CHARACTERISTICS, CLASS, EXSTYLE, MENU, STYLE, VERSION,
448 /// as well as VERSIONINFO-specific statements FILEFLAGSMASK, FILEFLAGS, FILEOS,
449 /// FILETYPE, FILESUBTYPE
450 pub const SimpleStatement = struct {
451 base: Node = .{ .id = .simple_statement },
452 identifier: Token,
453 value: *Node,
454 };
455
456 pub const Invalid = struct {
457 base: Node = .{ .id = .invalid },
458 context: []Token,
459 };
460
461 pub fn isNumberExpression(node: *const Node) bool {
462 switch (node.id) {
463 .literal => {
464 const literal = @fieldParentPtr(Node.Literal, "base", node);
465 return switch (literal.token.id) {
466 .number => true,
467 else => false,
468 };
469 },
470 .binary_expression, .grouped_expression, .not_expression => return true,
471 else => return false,
472 }
473 }
474
475 pub fn isStringLiteral(node: *const Node) bool {
476 switch (node.id) {
477 .literal => {
478 const literal = @fieldParentPtr(Node.Literal, "base", node);
479 return switch (literal.token.id) {
480 .quoted_ascii_string, .quoted_wide_string => true,
481 else => false,
482 };
483 },
484 else => return false,
485 }
486 }
487
488 pub fn getFirstToken(node: *const Node) Token {
489 switch (node.id) {
490 .root => unreachable,
491 .resource_external => {
492 const casted = @fieldParentPtr(Node.ResourceExternal, "base", node);
493 return casted.id;
494 },
495 .resource_raw_data => {
496 const casted = @fieldParentPtr(Node.ResourceRawData, "base", node);
497 return casted.id;
498 },
499 .literal => {
500 const casted = @fieldParentPtr(Node.Literal, "base", node);
501 return casted.token;
502 },
503 .binary_expression => {
504 const casted = @fieldParentPtr(Node.BinaryExpression, "base", node);
505 return casted.left.getFirstToken();
506 },
507 .grouped_expression => {
508 const casted = @fieldParentPtr(Node.GroupedExpression, "base", node);
509 return casted.open_token;
510 },
511 .not_expression => {
512 const casted = @fieldParentPtr(Node.NotExpression, "base", node);
513 return casted.not_token;
514 },
515 .accelerators => {
516 const casted = @fieldParentPtr(Node.Accelerators, "base", node);
517 return casted.id;
518 },
519 .accelerator => {
520 const casted = @fieldParentPtr(Node.Accelerator, "base", node);
521 return casted.event.getFirstToken();
522 },
523 .dialog => {
524 const casted = @fieldParentPtr(Node.Dialog, "base", node);
525 return casted.id;
526 },
527 .control_statement => {
528 const casted = @fieldParentPtr(Node.ControlStatement, "base", node);
529 return casted.type;
530 },
531 .toolbar => {
532 const casted = @fieldParentPtr(Node.Toolbar, "base", node);
533 return casted.id;
534 },
535 .menu => {
536 const casted = @fieldParentPtr(Node.Menu, "base", node);
537 return casted.id;
538 },
539 inline .menu_item, .menu_item_separator, .menu_item_ex => |menu_item_type| {
540 const node_type = menu_item_type.Type();
541 const casted = @fieldParentPtr(node_type, "base", node);
542 return casted.menuitem;
543 },
544 inline .popup, .popup_ex => |popup_type| {
545 const node_type = popup_type.Type();
546 const casted = @fieldParentPtr(node_type, "base", node);
547 return casted.popup;
548 },
549 .version_info => {
550 const casted = @fieldParentPtr(Node.VersionInfo, "base", node);
551 return casted.id;
552 },
553 .version_statement => {
554 const casted = @fieldParentPtr(Node.VersionStatement, "base", node);
555 return casted.type;
556 },
557 .block => {
558 const casted = @fieldParentPtr(Node.Block, "base", node);
559 return casted.identifier;
560 },
561 .block_value => {
562 const casted = @fieldParentPtr(Node.BlockValue, "base", node);
563 return casted.identifier;
564 },
565 .block_value_value => {
566 const casted = @fieldParentPtr(Node.BlockValueValue, "base", node);
567 return casted.expression.getFirstToken();
568 },
569 .string_table => {
570 const casted = @fieldParentPtr(Node.StringTable, "base", node);
571 return casted.type;
572 },
573 .string_table_string => {
574 const casted = @fieldParentPtr(Node.StringTableString, "base", node);
575 return casted.id.getFirstToken();
576 },
577 .language_statement => {
578 const casted = @fieldParentPtr(Node.LanguageStatement, "base", node);
579 return casted.language_token;
580 },
581 .font_statement => {
582 const casted = @fieldParentPtr(Node.FontStatement, "base", node);
583 return casted.identifier;
584 },
585 .simple_statement => {
586 const casted = @fieldParentPtr(Node.SimpleStatement, "base", node);
587 return casted.identifier;
588 },
589 .invalid => {
590 const casted = @fieldParentPtr(Node.Invalid, "base", node);
591 return casted.context[0];
592 },
593 }
594 }
595
596 pub fn getLastToken(node: *const Node) Token {
597 switch (node.id) {
598 .root => unreachable,
599 .resource_external => {
600 const casted = @fieldParentPtr(Node.ResourceExternal, "base", node);
601 return casted.filename.getLastToken();
602 },
603 .resource_raw_data => {
604 const casted = @fieldParentPtr(Node.ResourceRawData, "base", node);
605 return casted.end_token;
606 },
607 .literal => {
608 const casted = @fieldParentPtr(Node.Literal, "base", node);
609 return casted.token;
610 },
611 .binary_expression => {
612 const casted = @fieldParentPtr(Node.BinaryExpression, "base", node);
613 return casted.right.getLastToken();
614 },
615 .grouped_expression => {
616 const casted = @fieldParentPtr(Node.GroupedExpression, "base", node);
617 return casted.close_token;
618 },
619 .not_expression => {
620 const casted = @fieldParentPtr(Node.NotExpression, "base", node);
621 return casted.number_token;
622 },
623 .accelerators => {
624 const casted = @fieldParentPtr(Node.Accelerators, "base", node);
625 return casted.end_token;
626 },
627 .accelerator => {
628 const casted = @fieldParentPtr(Node.Accelerator, "base", node);
629 if (casted.type_and_options.len > 0) return casted.type_and_options[casted.type_and_options.len - 1];
630 return casted.idvalue.getLastToken();
631 },
632 .dialog => {
633 const casted = @fieldParentPtr(Node.Dialog, "base", node);
634 return casted.end_token;
635 },
636 .control_statement => {
637 const casted = @fieldParentPtr(Node.ControlStatement, "base", node);
638 if (casted.extra_data_end) |token| return token;
639 if (casted.help_id) |help_id_node| return help_id_node.getLastToken();
640 if (casted.exstyle) |exstyle_node| return exstyle_node.getLastToken();
641 // For user-defined CONTROL controls, the style comes before 'x', but
642 // otherwise it comes after 'height' so it could be the last token if
643 // it's present.
644 if (!casted.isUserDefined()) {
645 if (casted.style) |style_node| return style_node.getLastToken();
646 }
647 return casted.height.getLastToken();
648 },
649 .toolbar => {
650 const casted = @fieldParentPtr(Node.Toolbar, "base", node);
651 return casted.end_token;
652 },
653 .menu => {
654 const casted = @fieldParentPtr(Node.Menu, "base", node);
655 return casted.end_token;
656 },
657 .menu_item => {
658 const casted = @fieldParentPtr(Node.MenuItem, "base", node);
659 if (casted.option_list.len > 0) return casted.option_list[casted.option_list.len - 1];
660 return casted.result.getLastToken();
661 },
662 .menu_item_separator => {
663 const casted = @fieldParentPtr(Node.MenuItemSeparator, "base", node);
664 return casted.separator;
665 },
666 .menu_item_ex => {
667 const casted = @fieldParentPtr(Node.MenuItemEx, "base", node);
668 if (casted.state) |state_node| return state_node.getLastToken();
669 if (casted.type) |type_node| return type_node.getLastToken();
670 if (casted.id) |id_node| return id_node.getLastToken();
671 return casted.text;
672 },
673 inline .popup, .popup_ex => |popup_type| {
674 const node_type = popup_type.Type();
675 const casted = @fieldParentPtr(node_type, "base", node);
676 return casted.end_token;
677 },
678 .version_info => {
679 const casted = @fieldParentPtr(Node.VersionInfo, "base", node);
680 return casted.end_token;
681 },
682 .version_statement => {
683 const casted = @fieldParentPtr(Node.VersionStatement, "base", node);
684 return casted.parts[casted.parts.len - 1].getLastToken();
685 },
686 .block => {
687 const casted = @fieldParentPtr(Node.Block, "base", node);
688 return casted.end_token;
689 },
690 .block_value => {
691 const casted = @fieldParentPtr(Node.BlockValue, "base", node);
692 if (casted.values.len > 0) return casted.values[casted.values.len - 1].getLastToken();
693 return casted.key;
694 },
695 .block_value_value => {
696 const casted = @fieldParentPtr(Node.BlockValueValue, "base", node);
697 return casted.expression.getLastToken();
698 },
699 .string_table => {
700 const casted = @fieldParentPtr(Node.StringTable, "base", node);
701 return casted.end_token;
702 },
703 .string_table_string => {
704 const casted = @fieldParentPtr(Node.StringTableString, "base", node);
705 return casted.string;
706 },
707 .language_statement => {
708 const casted = @fieldParentPtr(Node.LanguageStatement, "base", node);
709 return casted.sublanguage_id.getLastToken();
710 },
711 .font_statement => {
712 const casted = @fieldParentPtr(Node.FontStatement, "base", node);
713 if (casted.char_set) |char_set_node| return char_set_node.getLastToken();
714 if (casted.italic) |italic_node| return italic_node.getLastToken();
715 if (casted.weight) |weight_node| return weight_node.getLastToken();
716 return casted.typeface;
717 },
718 .simple_statement => {
719 const casted = @fieldParentPtr(Node.SimpleStatement, "base", node);
720 return casted.value.getLastToken();
721 },
722 .invalid => {
723 const casted = @fieldParentPtr(Node.Invalid, "base", node);
724 return casted.context[casted.context.len - 1];
725 },
726 }
727 }
728
729 pub fn dump(
730 node: *const Node,
731 tree: *const Tree,
732 writer: anytype,
733 indent: usize,
734 ) @TypeOf(writer).Error!void {
735 try writer.writeByteNTimes(' ', indent);
736 try writer.writeAll(@tagName(node.id));
737 switch (node.id) {
738 .root => {
739 try writer.writeAll("\n");
740 const root = @fieldParentPtr(Node.Root, "base", node);
741 for (root.body) |body_node| {
742 try body_node.dump(tree, writer, indent + 1);
743 }
744 },
745 .resource_external => {
746 const resource = @fieldParentPtr(Node.ResourceExternal, "base", node);
747 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len });
748 try resource.filename.dump(tree, writer, indent + 1);
749 },
750 .resource_raw_data => {
751 const resource = @fieldParentPtr(Node.ResourceRawData, "base", node);
752 try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len });
753 for (resource.raw_data) |data_expression| {
754 try data_expression.dump(tree, writer, indent + 1);
755 }
756 },
757 .literal => {
758 const literal = @fieldParentPtr(Node.Literal, "base", node);
759 try writer.writeAll(" ");
760 try writer.writeAll(literal.token.slice(tree.source));
761 try writer.writeAll("\n");
762 },
763 .binary_expression => {
764 const binary = @fieldParentPtr(Node.BinaryExpression, "base", node);
765 try writer.writeAll(" ");
766 try writer.writeAll(binary.operator.slice(tree.source));
767 try writer.writeAll("\n");
768 try binary.left.dump(tree, writer, indent + 1);
769 try binary.right.dump(tree, writer, indent + 1);
770 },
771 .grouped_expression => {
772 const grouped = @fieldParentPtr(Node.GroupedExpression, "base", node);
773 try writer.writeAll("\n");
774 try writer.writeByteNTimes(' ', indent);
775 try writer.writeAll(grouped.open_token.slice(tree.source));
776 try writer.writeAll("\n");
777 try grouped.expression.dump(tree, writer, indent + 1);
778 try writer.writeByteNTimes(' ', indent);
779 try writer.writeAll(grouped.close_token.slice(tree.source));
780 try writer.writeAll("\n");
781 },
782 .not_expression => {
783 const not = @fieldParentPtr(Node.NotExpression, "base", node);
784 try writer.writeAll(" ");
785 try writer.writeAll(not.not_token.slice(tree.source));
786 try writer.writeAll(" ");
787 try writer.writeAll(not.number_token.slice(tree.source));
788 try writer.writeAll("\n");
789 },
790 .accelerators => {
791 const accelerators = @fieldParentPtr(Node.Accelerators, "base", node);
792 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len });
793 for (accelerators.optional_statements) |statement| {
794 try statement.dump(tree, writer, indent + 1);
795 }
796 try writer.writeByteNTimes(' ', indent);
797 try writer.writeAll(accelerators.begin_token.slice(tree.source));
798 try writer.writeAll("\n");
799 for (accelerators.accelerators) |accelerator| {
800 try accelerator.dump(tree, writer, indent + 1);
801 }
802 try writer.writeByteNTimes(' ', indent);
803 try writer.writeAll(accelerators.end_token.slice(tree.source));
804 try writer.writeAll("\n");
805 },
806 .accelerator => {
807 const accelerator = @fieldParentPtr(Node.Accelerator, "base", node);
808 for (accelerator.type_and_options, 0..) |option, i| {
809 if (i != 0) try writer.writeAll(",");
810 try writer.writeByte(' ');
811 try writer.writeAll(option.slice(tree.source));
812 }
813 try writer.writeAll("\n");
814 try accelerator.event.dump(tree, writer, indent + 1);
815 try accelerator.idvalue.dump(tree, writer, indent + 1);
816 },
817 .dialog => {
818 const dialog = @fieldParentPtr(Node.Dialog, "base", node);
819 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len });
820 inline for (.{ "x", "y", "width", "height" }) |arg| {
821 try writer.writeByteNTimes(' ', indent + 1);
822 try writer.writeAll(arg ++ ":\n");
823 try @field(dialog, arg).dump(tree, writer, indent + 2);
824 }
825 if (dialog.help_id) |help_id| {
826 try writer.writeByteNTimes(' ', indent + 1);
827 try writer.writeAll("help_id:\n");
828 try help_id.dump(tree, writer, indent + 2);
829 }
830 for (dialog.optional_statements) |statement| {
831 try statement.dump(tree, writer, indent + 1);
832 }
833 try writer.writeByteNTimes(' ', indent);
834 try writer.writeAll(dialog.begin_token.slice(tree.source));
835 try writer.writeAll("\n");
836 for (dialog.controls) |control| {
837 try control.dump(tree, writer, indent + 1);
838 }
839 try writer.writeByteNTimes(' ', indent);
840 try writer.writeAll(dialog.end_token.slice(tree.source));
841 try writer.writeAll("\n");
842 },
843 .control_statement => {
844 const control = @fieldParentPtr(Node.ControlStatement, "base", node);
845 try writer.print(" {s}", .{control.type.slice(tree.source)});
846 if (control.text) |text| {
847 try writer.print(" text: {s}", .{text.slice(tree.source)});
848 }
849 try writer.writeByte('\n');
850 if (control.class) |class| {
851 try writer.writeByteNTimes(' ', indent + 1);
852 try writer.writeAll("class:\n");
853 try class.dump(tree, writer, indent + 2);
854 }
855 inline for (.{ "id", "x", "y", "width", "height" }) |arg| {
856 try writer.writeByteNTimes(' ', indent + 1);
857 try writer.writeAll(arg ++ ":\n");
858 try @field(control, arg).dump(tree, writer, indent + 2);
859 }
860 inline for (.{ "style", "exstyle", "help_id" }) |arg| {
861 if (@field(control, arg)) |val_node| {
862 try writer.writeByteNTimes(' ', indent + 1);
863 try writer.writeAll(arg ++ ":\n");
864 try val_node.dump(tree, writer, indent + 2);
865 }
866 }
867 if (control.extra_data_begin != null) {
868 try writer.writeByteNTimes(' ', indent);
869 try writer.writeAll(control.extra_data_begin.?.slice(tree.source));
870 try writer.writeAll("\n");
871 for (control.extra_data) |data_node| {
872 try data_node.dump(tree, writer, indent + 1);
873 }
874 try writer.writeByteNTimes(' ', indent);
875 try writer.writeAll(control.extra_data_end.?.slice(tree.source));
876 try writer.writeAll("\n");
877 }
878 },
879 .toolbar => {
880 const toolbar = @fieldParentPtr(Node.Toolbar, "base", node);
881 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len });
882 inline for (.{ "button_width", "button_height" }) |arg| {
883 try writer.writeByteNTimes(' ', indent + 1);
884 try writer.writeAll(arg ++ ":\n");
885 try @field(toolbar, arg).dump(tree, writer, indent + 2);
886 }
887 try writer.writeByteNTimes(' ', indent);
888 try writer.writeAll(toolbar.begin_token.slice(tree.source));
889 try writer.writeAll("\n");
890 for (toolbar.buttons) |button_or_sep| {
891 try button_or_sep.dump(tree, writer, indent + 1);
892 }
893 try writer.writeByteNTimes(' ', indent);
894 try writer.writeAll(toolbar.end_token.slice(tree.source));
895 try writer.writeAll("\n");
896 },
897 .menu => {
898 const menu = @fieldParentPtr(Node.Menu, "base", node);
899 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len });
900 for (menu.optional_statements) |statement| {
901 try statement.dump(tree, writer, indent + 1);
902 }
903 if (menu.help_id) |help_id| {
904 try writer.writeByteNTimes(' ', indent + 1);
905 try writer.writeAll("help_id:\n");
906 try help_id.dump(tree, writer, indent + 2);
907 }
908 try writer.writeByteNTimes(' ', indent);
909 try writer.writeAll(menu.begin_token.slice(tree.source));
910 try writer.writeAll("\n");
911 for (menu.items) |item| {
912 try item.dump(tree, writer, indent + 1);
913 }
914 try writer.writeByteNTimes(' ', indent);
915 try writer.writeAll(menu.end_token.slice(tree.source));
916 try writer.writeAll("\n");
917 },
918 .menu_item => {
919 const menu_item = @fieldParentPtr(Node.MenuItem, "base", node);
920 try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len });
921 try menu_item.result.dump(tree, writer, indent + 1);
922 },
923 .menu_item_separator => {
924 const menu_item = @fieldParentPtr(Node.MenuItemSeparator, "base", node);
925 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) });
926 },
927 .menu_item_ex => {
928 const menu_item = @fieldParentPtr(Node.MenuItemEx, "base", node);
929 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) });
930 inline for (.{ "id", "type", "state" }) |arg| {
931 if (@field(menu_item, arg)) |val_node| {
932 try writer.writeByteNTimes(' ', indent + 1);
933 try writer.writeAll(arg ++ ":\n");
934 try val_node.dump(tree, writer, indent + 2);
935 }
936 }
937 },
938 .popup => {
939 const popup = @fieldParentPtr(Node.Popup, "base", node);
940 try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len });
941 try writer.writeByteNTimes(' ', indent);
942 try writer.writeAll(popup.begin_token.slice(tree.source));
943 try writer.writeAll("\n");
944 for (popup.items) |item| {
945 try item.dump(tree, writer, indent + 1);
946 }
947 try writer.writeByteNTimes(' ', indent);
948 try writer.writeAll(popup.end_token.slice(tree.source));
949 try writer.writeAll("\n");
950 },
951 .popup_ex => {
952 const popup = @fieldParentPtr(Node.PopupEx, "base", node);
953 try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) });
954 inline for (.{ "id", "type", "state", "help_id" }) |arg| {
955 if (@field(popup, arg)) |val_node| {
956 try writer.writeByteNTimes(' ', indent + 1);
957 try writer.writeAll(arg ++ ":\n");
958 try val_node.dump(tree, writer, indent + 2);
959 }
960 }
961 try writer.writeByteNTimes(' ', indent);
962 try writer.writeAll(popup.begin_token.slice(tree.source));
963 try writer.writeAll("\n");
964 for (popup.items) |item| {
965 try item.dump(tree, writer, indent + 1);
966 }
967 try writer.writeByteNTimes(' ', indent);
968 try writer.writeAll(popup.end_token.slice(tree.source));
969 try writer.writeAll("\n");
970 },
971 .version_info => {
972 const version_info = @fieldParentPtr(Node.VersionInfo, "base", node);
973 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len });
974 for (version_info.fixed_info) |fixed_info| {
975 try fixed_info.dump(tree, writer, indent + 1);
976 }
977 try writer.writeByteNTimes(' ', indent);
978 try writer.writeAll(version_info.begin_token.slice(tree.source));
979 try writer.writeAll("\n");
980 for (version_info.block_statements) |block| {
981 try block.dump(tree, writer, indent + 1);
982 }
983 try writer.writeByteNTimes(' ', indent);
984 try writer.writeAll(version_info.end_token.slice(tree.source));
985 try writer.writeAll("\n");
986 },
987 .version_statement => {
988 const version_statement = @fieldParentPtr(Node.VersionStatement, "base", node);
989 try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)});
990 for (version_statement.parts) |part| {
991 try part.dump(tree, writer, indent + 1);
992 }
993 },
994 .block => {
995 const block = @fieldParentPtr(Node.Block, "base", node);
996 try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) });
997 for (block.values) |value| {
998 try value.dump(tree, writer, indent + 1);
999 }
1000 try writer.writeByteNTimes(' ', indent);
1001 try writer.writeAll(block.begin_token.slice(tree.source));
1002 try writer.writeAll("\n");
1003 for (block.children) |child| {
1004 try child.dump(tree, writer, indent + 1);
1005 }
1006 try writer.writeByteNTimes(' ', indent);
1007 try writer.writeAll(block.end_token.slice(tree.source));
1008 try writer.writeAll("\n");
1009 },
1010 .block_value => {
1011 const block_value = @fieldParentPtr(Node.BlockValue, "base", node);
1012 try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) });
1013 for (block_value.values) |value| {
1014 try value.dump(tree, writer, indent + 1);
1015 }
1016 },
1017 .block_value_value => {
1018 const block_value = @fieldParentPtr(Node.BlockValueValue, "base", node);
1019 if (block_value.trailing_comma) {
1020 try writer.writeAll(" ,");
1021 }
1022 try writer.writeAll("\n");
1023 try block_value.expression.dump(tree, writer, indent + 1);
1024 },
1025 .string_table => {
1026 const string_table = @fieldParentPtr(Node.StringTable, "base", node);
1027 try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len });
1028 for (string_table.optional_statements) |statement| {
1029 try statement.dump(tree, writer, indent + 1);
1030 }
1031 try writer.writeByteNTimes(' ', indent);
1032 try writer.writeAll(string_table.begin_token.slice(tree.source));
1033 try writer.writeAll("\n");
1034 for (string_table.strings) |string| {
1035 try string.dump(tree, writer, indent + 1);
1036 }
1037 try writer.writeByteNTimes(' ', indent);
1038 try writer.writeAll(string_table.end_token.slice(tree.source));
1039 try writer.writeAll("\n");
1040 },
1041 .string_table_string => {
1042 try writer.writeAll("\n");
1043 const string = @fieldParentPtr(Node.StringTableString, "base", node);
1044 try string.id.dump(tree, writer, indent + 1);
1045 try writer.writeByteNTimes(' ', indent + 1);
1046 try writer.print("{s}\n", .{string.string.slice(tree.source)});
1047 },
1048 .language_statement => {
1049 const language = @fieldParentPtr(Node.LanguageStatement, "base", node);
1050 try writer.print(" {s}\n", .{language.language_token.slice(tree.source)});
1051 try language.primary_language_id.dump(tree, writer, indent + 1);
1052 try language.sublanguage_id.dump(tree, writer, indent + 1);
1053 },
1054 .font_statement => {
1055 const font = @fieldParentPtr(Node.FontStatement, "base", node);
1056 try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) });
1057 try writer.writeByteNTimes(' ', indent + 1);
1058 try writer.writeAll("point_size:\n");
1059 try font.point_size.dump(tree, writer, indent + 2);
1060 inline for (.{ "weight", "italic", "char_set" }) |arg| {
1061 if (@field(font, arg)) |arg_node| {
1062 try writer.writeByteNTimes(' ', indent + 1);
1063 try writer.writeAll(arg ++ ":\n");
1064 try arg_node.dump(tree, writer, indent + 2);
1065 }
1066 }
1067 },
1068 .simple_statement => {
1069 const statement = @fieldParentPtr(Node.SimpleStatement, "base", node);
1070 try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)});
1071 try statement.value.dump(tree, writer, indent + 1);
1072 },
1073 .invalid => {
1074 const invalid = @fieldParentPtr(Node.Invalid, "base", node);
1075 try writer.print(" context.len: {}\n", .{invalid.context.len});
1076 for (invalid.context) |context_token| {
1077 try writer.writeByteNTimes(' ', indent + 1);
1078 try writer.print("{s}:{s}", .{ @tagName(context_token.id), context_token.slice(tree.source) });
1079 try writer.writeByte('\n');
1080 }
1081 },
1082 }
1083 }
1084};
src/resinator/bmp.zig deleted-270
...@@ -1,270 +0,0 @@
1//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
2//! https://learn.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)
3//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
4//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
5//! https://archive.org/details/mac_Graphics_File_Formats_Second_Edition_1996/page/n607/mode/2up
6//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
7//!
8//! Notes:
9//! - The Microsoft documentation is incredibly unclear about the color table when the
10//! bit depth is >= 16.
11//! + For bit depth 24 it says "the bmiColors member of BITMAPINFO is NULL" but also
12//! says "the bmiColors color table is used for optimizing colors used on palette-based
13//! devices, and must contain the number of entries specified by the bV5ClrUsed member"
14//! + For bit depth 16 and 32, it seems to imply that if the compression is BI_BITFIELDS
15//! or BI_ALPHABITFIELDS, then the color table *only* consists of the bit masks, but
16//! doesn't really say this outright and the Wikipedia article seems to disagree
17//! For the purposes of this implementation, color tables can always be present for any
18//! bit depth and compression, and the color table follows the header + any optional
19//! bit mask fields dictated by the specified compression.
20
21const std = @import("std");
22const BitmapHeader = @import("ico.zig").BitmapHeader;
23const builtin = @import("builtin");
24const native_endian = builtin.cpu.arch.endian();
25
26pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian);
27pub const file_header_len = 14;
28
29pub const ReadError = error{
30 UnexpectedEOF,
31 InvalidFileHeader,
32 ImpossiblePixelDataOffset,
33 UnknownBitmapVersion,
34 InvalidBitsPerPixel,
35 TooManyColorsInPalette,
36 MissingBitfieldMasks,
37};
38
39pub const BitmapInfo = struct {
40 dib_header_size: u32,
41 /// Contains the interpreted number of colors in the palette (e.g.
42 /// if the field's value is zero and the bit depth is <= 8, this
43 /// will contain the maximum number of colors for the bit depth
44 /// rather than the field's value directly).
45 colors_in_palette: u32,
46 bytes_per_color_palette_element: u8,
47 pixel_data_offset: u32,
48 compression: Compression,
49
50 pub fn getExpectedPaletteByteLen(self: *const BitmapInfo) u64 {
51 return @as(u64, self.colors_in_palette) * self.bytes_per_color_palette_element;
52 }
53
54 pub fn getActualPaletteByteLen(self: *const BitmapInfo) u64 {
55 return self.getByteLenBetweenHeadersAndPixels() - self.getBitmasksByteLen();
56 }
57
58 pub fn getByteLenBetweenHeadersAndPixels(self: *const BitmapInfo) u64 {
59 return @as(u64, self.pixel_data_offset) - self.dib_header_size - file_header_len;
60 }
61
62 pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 {
63 return switch (self.compression) {
64 .BI_BITFIELDS => 12,
65 .BI_ALPHABITFIELDS => 16,
66 else => 0,
67 };
68 }
69
70 pub fn getMissingPaletteByteLen(self: *const BitmapInfo) u64 {
71 if (self.getActualPaletteByteLen() >= self.getExpectedPaletteByteLen()) return 0;
72 return self.getExpectedPaletteByteLen() - self.getActualPaletteByteLen();
73 }
74
75 /// Returns the full byte len of the DIB header + optional bitmasks + color palette
76 pub fn getExpectedByteLenBeforePixelData(self: *const BitmapInfo) u64 {
77 return @as(u64, self.dib_header_size) + self.getBitmasksByteLen() + self.getExpectedPaletteByteLen();
78 }
79
80 /// Returns the full expected byte len
81 pub fn getExpectedByteLen(self: *const BitmapInfo, file_size: u64) u64 {
82 return self.getExpectedByteLenBeforePixelData() + self.getPixelDataLen(file_size);
83 }
84
85 pub fn getPixelDataLen(self: *const BitmapInfo, file_size: u64) u64 {
86 return file_size - self.pixel_data_offset;
87 }
88};
89
90pub fn read(reader: anytype, max_size: u64) ReadError!BitmapInfo {
91 var bitmap_info: BitmapInfo = undefined;
92 const file_header = reader.readBytesNoEof(file_header_len) catch return error.UnexpectedEOF;
93
94 const id = std.mem.readInt(u16, file_header[0..2], native_endian);
95 if (id != windows_format_id) return error.InvalidFileHeader;
96
97 bitmap_info.pixel_data_offset = std.mem.readInt(u32, file_header[10..14], .little);
98 if (bitmap_info.pixel_data_offset > max_size) return error.ImpossiblePixelDataOffset;
99
100 bitmap_info.dib_header_size = reader.readInt(u32, .little) catch return error.UnexpectedEOF;
101 if (bitmap_info.pixel_data_offset < file_header_len + bitmap_info.dib_header_size) return error.ImpossiblePixelDataOffset;
102 const dib_version = BitmapHeader.Version.get(bitmap_info.dib_header_size);
103 switch (dib_version) {
104 .@"nt3.1", .@"nt4.0", .@"nt5.0" => {
105 var dib_header_buf: [@sizeOf(BITMAPINFOHEADER)]u8 align(@alignOf(BITMAPINFOHEADER)) = undefined;
106 std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
107 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF;
108 var dib_header: *BITMAPINFOHEADER = @ptrCast(&dib_header_buf);
109 structFieldsLittleToNative(BITMAPINFOHEADER, dib_header);
110
111 bitmap_info.colors_in_palette = try dib_header.numColorsInTable();
112 bitmap_info.bytes_per_color_palette_element = 4;
113 bitmap_info.compression = @enumFromInt(dib_header.biCompression);
114
115 if (bitmap_info.getByteLenBetweenHeadersAndPixels() < bitmap_info.getBitmasksByteLen()) {
116 return error.MissingBitfieldMasks;
117 }
118 },
119 .@"win2.0" => {
120 var dib_header_buf: [@sizeOf(BITMAPCOREHEADER)]u8 align(@alignOf(BITMAPCOREHEADER)) = undefined;
121 std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
122 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF;
123 const dib_header: *BITMAPCOREHEADER = @ptrCast(&dib_header_buf);
124 structFieldsLittleToNative(BITMAPCOREHEADER, dib_header);
125
126 // > The size of the color palette is calculated from the BitsPerPixel value.
127 // > The color palette has 2, 16, 256, or 0 entries for a BitsPerPixel of
128 // > 1, 4, 8, and 24, respectively.
129 bitmap_info.colors_in_palette = switch (dib_header.bcBitCount) {
130 inline 1, 4, 8 => |bit_count| 1 << bit_count,
131 24 => 0,
132 else => return error.InvalidBitsPerPixel,
133 };
134 bitmap_info.bytes_per_color_palette_element = 3;
135
136 bitmap_info.compression = .BI_RGB;
137 },
138 .unknown => return error.UnknownBitmapVersion,
139 }
140
141 return bitmap_info;
142}
143
144/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
145pub const BITMAPCOREHEADER = extern struct {
146 bcSize: u32,
147 bcWidth: u16,
148 bcHeight: u16,
149 bcPlanes: u16,
150 bcBitCount: u16,
151};
152
153/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
154pub const BITMAPINFOHEADER = extern struct {
155 bcSize: u32,
156 biWidth: i32,
157 biHeight: i32,
158 biPlanes: u16,
159 biBitCount: u16,
160 biCompression: u32,
161 biSizeImage: u32,
162 biXPelsPerMeter: i32,
163 biYPelsPerMeter: i32,
164 biClrUsed: u32,
165 biClrImportant: u32,
166
167 /// Returns error.TooManyColorsInPalette if the number of colors specified
168 /// exceeds the number of possible colors referenced in the pixel data (i.e.
169 /// if 1 bit is used per pixel, then the color table can't have more than 2 colors
170 /// since any more couldn't possibly be indexed in the pixel data)
171 ///
172 /// Returns error.InvalidBitsPerPixel if the bit depth is not 1, 4, 8, 16, 24, or 32.
173 pub fn numColorsInTable(self: BITMAPINFOHEADER) !u32 {
174 switch (self.biBitCount) {
175 inline 1, 4, 8 => |bit_count| switch (self.biClrUsed) {
176 // > If biClrUsed is zero, the array contains the maximum number of
177 // > colors for the given bitdepth; that is, 2^biBitCount colors
178 0 => return 1 << bit_count,
179 // > If biClrUsed is nonzero and the biBitCount member is less than 16,
180 // > the biClrUsed member specifies the actual number of colors the
181 // > graphics engine or device driver accesses.
182 else => {
183 const max_colors = 1 << bit_count;
184 if (self.biClrUsed > max_colors) {
185 return error.TooManyColorsInPalette;
186 }
187 return self.biClrUsed;
188 },
189 },
190 // > If biBitCount is 16 or greater, the biClrUsed member specifies
191 // > the size of the color table used to optimize performance of the
192 // > system color palettes.
193 //
194 // Note: Bit depths >= 16 only use the color table 'for optimizing colors
195 // used on palette-based devices', but it still makes sense to limit their
196 // colors since the pixel data is still limited to this number of colors
197 // (i.e. even though the color table is not indexed by the pixel data,
198 // the color table having more colors than the pixel data can represent
199 // would never make sense and indicates a malformed bitmap).
200 inline 16, 24, 32 => |bit_count| {
201 const max_colors = 1 << bit_count;
202 if (self.biClrUsed > max_colors) {
203 return error.TooManyColorsInPalette;
204 }
205 return self.biClrUsed;
206 },
207 else => return error.InvalidBitsPerPixel,
208 }
209 }
210};
211
212pub const Compression = enum(u32) {
213 BI_RGB = 0,
214 BI_RLE8 = 1,
215 BI_RLE4 = 2,
216 BI_BITFIELDS = 3,
217 BI_JPEG = 4,
218 BI_PNG = 5,
219 BI_ALPHABITFIELDS = 6,
220 BI_CMYK = 11,
221 BI_CMYKRLE8 = 12,
222 BI_CMYKRLE4 = 13,
223 _,
224};
225
226fn structFieldsLittleToNative(comptime T: type, x: *T) void {
227 inline for (@typeInfo(T).Struct.fields) |field| {
228 @field(x, field.name) = std.mem.littleToNative(field.type, @field(x, field.name));
229 }
230}
231
232test "read" {
233 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".*;
234 var fbs = std.io.fixedBufferStream(&bmp_data);
235
236 {
237 const bitmap = try read(fbs.reader(), bmp_data.len);
238 try std.testing.expectEqual(@as(u32, BitmapHeader.Version.@"nt3.1".len()), bitmap.dib_header_size);
239 }
240
241 {
242 fbs.reset();
243 bmp_data[file_header_len] = 11;
244 try std.testing.expectError(error.UnknownBitmapVersion, read(fbs.reader(), bmp_data.len));
245
246 // restore
247 bmp_data[file_header_len] = BitmapHeader.Version.@"nt3.1".len();
248 }
249
250 {
251 fbs.reset();
252 bmp_data[0] = 'b';
253 try std.testing.expectError(error.InvalidFileHeader, read(fbs.reader(), bmp_data.len));
254
255 // restore
256 bmp_data[0] = 'B';
257 }
258
259 {
260 const cutoff_len = file_header_len + BitmapHeader.Version.@"nt3.1".len() - 1;
261 var dib_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]);
262 try std.testing.expectError(error.UnexpectedEOF, read(dib_cutoff_fbs.reader(), bmp_data.len));
263 }
264
265 {
266 const cutoff_len = file_header_len - 1;
267 var bmp_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]);
268 try std.testing.expectError(error.UnexpectedEOF, read(bmp_cutoff_fbs.reader(), bmp_data.len));
269 }
270}
src/resinator/cli.zig deleted-1439
...@@ -1,1439 +0,0 @@
1const std = @import("std");
2const CodePage = @import("code_pages.zig").CodePage;
3const lang = @import("lang.zig");
4const res = @import("res.zig");
5const Allocator = std.mem.Allocator;
6const lex = @import("lex.zig");
7
8/// This is what /SL 100 will set the maximum string literal length to
9pub const max_string_literal_length_100_percent = 8192;
10
11pub const usage_string_after_command_name =
12 \\ [options] [--] <INPUT> [<OUTPUT>]
13 \\
14 \\The sequence -- can be used to signify when to stop parsing options.
15 \\This is necessary when the input path begins with a forward slash.
16 \\
17 \\Supported Win32 RC Options:
18 \\ /?, /h Print this help and exit.
19 \\ /v Verbose (print progress messages).
20 \\ /d <name>[=<value>] Define a symbol (during preprocessing).
21 \\ /u <name> Undefine a symbol (during preprocessing).
22 \\ /fo <value> Specify output file path.
23 \\ /l <value> Set default language using hexadecimal id (ex: 409).
24 \\ /ln <value> Set default language using language name (ex: en-us).
25 \\ /i <value> Add an include path.
26 \\ /x Ignore INCLUDE environment variable.
27 \\ /c <value> Set default code page (ex: 65001).
28 \\ /w Warn on invalid code page in .rc (instead of error).
29 \\ /y Suppress warnings for duplicate control IDs.
30 \\ /n Null-terminate all strings in string tables.
31 \\ /sl <value> Specify string literal length limit in percentage (1-100)
32 \\ where 100 corresponds to a limit of 8192. If the /sl
33 \\ option is not specified, the default limit is 4097.
34 \\ /p Only run the preprocessor and output a .rcpp file.
35 \\
36 \\No-op Win32 RC Options:
37 \\ /nologo, /a, /r Options that are recognized but do nothing.
38 \\
39 \\Unsupported Win32 RC Options:
40 \\ /fm, /q, /g, /gn, /g1, /g2 Unsupported MUI-related options.
41 \\ /?c, /hc, /t, /tp:<prefix>, Unsupported LCX/LCE-related options.
42 \\ /tn, /tm, /tc, /tw, /te,
43 \\ /ti, /ta
44 \\ /z Unsupported font-substitution-related option.
45 \\ /s Unsupported HWB-related option.
46 \\
47 \\Custom Options (resinator-specific):
48 \\ /:no-preprocess Do not run the preprocessor.
49 \\ /:debug Output the preprocessed .rc file and the parsed AST.
50 \\ /:auto-includes <value> Set the automatic include path detection behavior.
51 \\ any (default) Use MSVC if available, fall back to MinGW
52 \\ msvc Use MSVC include paths (must be present on the system)
53 \\ gnu Use MinGW include paths (requires Zig as the preprocessor)
54 \\ none Do not use any autodetected include paths
55 \\
56 \\Note: For compatibility reasons, all custom options start with :
57 \\
58;
59
60pub fn writeUsage(writer: anytype, command_name: []const u8) !void {
61 try writer.writeAll("Usage: ");
62 try writer.writeAll(command_name);
63 try writer.writeAll(usage_string_after_command_name);
64}
65
66pub const Diagnostics = struct {
67 errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
68 allocator: Allocator,
69
70 pub const ErrorDetails = struct {
71 arg_index: usize,
72 arg_span: ArgSpan = .{},
73 msg: std.ArrayListUnmanaged(u8) = .{},
74 type: Type = .err,
75 print_args: bool = true,
76
77 pub const Type = enum { err, warning, note };
78 pub const ArgSpan = struct {
79 point_at_next_arg: bool = false,
80 name_offset: usize = 0,
81 prefix_len: usize = 0,
82 value_offset: usize = 0,
83 name_len: usize = 0,
84 };
85 };
86
87 pub fn init(allocator: Allocator) Diagnostics {
88 return .{
89 .allocator = allocator,
90 };
91 }
92
93 pub fn deinit(self: *Diagnostics) void {
94 for (self.errors.items) |*details| {
95 details.msg.deinit(self.allocator);
96 }
97 self.errors.deinit(self.allocator);
98 }
99
100 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
101 try self.errors.append(self.allocator, error_details);
102 }
103
104 pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8, config: std.io.tty.Config) void {
105 std.debug.getStderrMutex().lock();
106 defer std.debug.getStderrMutex().unlock();
107 const stderr = std.io.getStdErr().writer();
108 self.renderToWriter(args, stderr, config) catch return;
109 }
110
111 pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: anytype, config: std.io.tty.Config) !void {
112 for (self.errors.items) |err_details| {
113 try renderErrorMessage(writer, config, err_details, args);
114 }
115 }
116
117 pub fn hasError(self: *const Diagnostics) bool {
118 for (self.errors.items) |err| {
119 if (err.type == .err) return true;
120 }
121 return false;
122 }
123};
124
125pub const Options = struct {
126 allocator: Allocator,
127 input_filename: []const u8 = &[_]u8{},
128 output_filename: []const u8 = &[_]u8{},
129 extra_include_paths: std.ArrayListUnmanaged([]const u8) = .{},
130 ignore_include_env_var: bool = false,
131 preprocess: Preprocess = .yes,
132 default_language_id: ?u16 = null,
133 default_code_page: ?CodePage = null,
134 verbose: bool = false,
135 symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .{},
136 null_terminate_string_table_strings: bool = false,
137 max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
138 silent_duplicate_control_ids: bool = false,
139 warn_instead_of_error_on_invalid_code_page: bool = false,
140 debug: bool = false,
141 print_help_and_exit: bool = false,
142 auto_includes: AutoIncludes = .any,
143
144 pub const AutoIncludes = enum { any, msvc, gnu, none };
145 pub const Preprocess = enum { no, yes, only };
146 pub const SymbolAction = enum { define, undefine };
147 pub const SymbolValue = union(SymbolAction) {
148 define: []const u8,
149 undefine: void,
150
151 pub fn deinit(self: SymbolValue, allocator: Allocator) void {
152 switch (self) {
153 .define => |value| allocator.free(value),
154 .undefine => {},
155 }
156 }
157 };
158
159 /// Does not check that identifier contains only valid characters
160 pub fn define(self: *Options, identifier: []const u8, value: []const u8) !void {
161 if (self.symbols.getPtr(identifier)) |val_ptr| {
162 // If the symbol is undefined, then that always takes precedence so
163 // we shouldn't change anything.
164 if (val_ptr.* == .undefine) return;
165 // Otherwise, the new value takes precedence.
166 const duped_value = try self.allocator.dupe(u8, value);
167 errdefer self.allocator.free(duped_value);
168 val_ptr.deinit(self.allocator);
169 val_ptr.* = .{ .define = duped_value };
170 return;
171 }
172 const duped_key = try self.allocator.dupe(u8, identifier);
173 errdefer self.allocator.free(duped_key);
174 const duped_value = try self.allocator.dupe(u8, value);
175 errdefer self.allocator.free(duped_value);
176 try self.symbols.put(self.allocator, duped_key, .{ .define = duped_value });
177 }
178
179 /// Does not check that identifier contains only valid characters
180 pub fn undefine(self: *Options, identifier: []const u8) !void {
181 if (self.symbols.getPtr(identifier)) |action| {
182 action.deinit(self.allocator);
183 action.* = .{ .undefine = {} };
184 return;
185 }
186 const duped_key = try self.allocator.dupe(u8, identifier);
187 errdefer self.allocator.free(duped_key);
188 try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} });
189 }
190
191 /// If the current input filename both:
192 /// - does not have an extension, and
193 /// - does not exist in the cwd
194 /// then this function will append `.rc` to the input filename
195 ///
196 /// Note: This behavior is different from the Win32 compiler.
197 /// It always appends .RC if the filename does not have
198 /// a `.` in it and it does not even try the verbatim name
199 /// in that scenario.
200 ///
201 /// The approach taken here is meant to give us a 'best of both
202 /// worlds' situation where we'll be compatible with most use-cases
203 /// of the .rc extension being omitted from the CLI args, but still
204 /// work fine if the file itself does not have an extension.
205 pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void {
206 if (std.fs.path.extension(options.input_filename).len == 0) {
207 cwd.access(options.input_filename, .{}) catch |err| switch (err) {
208 error.FileNotFound => {
209 var filename_bytes = try options.allocator.alloc(u8, options.input_filename.len + 3);
210 @memcpy(filename_bytes[0 .. filename_bytes.len - 3], options.input_filename);
211 @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc");
212 options.allocator.free(options.input_filename);
213 options.input_filename = filename_bytes;
214 },
215 else => {},
216 };
217 }
218 }
219
220 pub fn deinit(self: *Options) void {
221 for (self.extra_include_paths.items) |extra_include_path| {
222 self.allocator.free(extra_include_path);
223 }
224 self.extra_include_paths.deinit(self.allocator);
225 self.allocator.free(self.input_filename);
226 self.allocator.free(self.output_filename);
227 var symbol_it = self.symbols.iterator();
228 while (symbol_it.next()) |entry| {
229 self.allocator.free(entry.key_ptr.*);
230 entry.value_ptr.deinit(self.allocator);
231 }
232 self.symbols.deinit(self.allocator);
233 }
234
235 pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
236 try writer.print("Input filename: {s}\n", .{self.input_filename});
237 try writer.print("Output filename: {s}\n", .{self.output_filename});
238 if (self.extra_include_paths.items.len > 0) {
239 try writer.writeAll(" Extra include paths:\n");
240 for (self.extra_include_paths.items) |extra_include_path| {
241 try writer.print(" \"{s}\"\n", .{extra_include_path});
242 }
243 }
244 if (self.ignore_include_env_var) {
245 try writer.writeAll(" The INCLUDE environment variable will be ignored\n");
246 }
247 if (self.preprocess == .no) {
248 try writer.writeAll(" The preprocessor will not be invoked\n");
249 } else if (self.preprocess == .only) {
250 try writer.writeAll(" Only the preprocessor will be invoked\n");
251 }
252 if (self.symbols.count() > 0) {
253 try writer.writeAll(" Symbols:\n");
254 var it = self.symbols.iterator();
255 while (it.next()) |symbol| {
256 try writer.print(" {s} {s}", .{ switch (symbol.value_ptr.*) {
257 .define => "#define",
258 .undefine => "#undef",
259 }, symbol.key_ptr.* });
260 if (symbol.value_ptr.* == .define) {
261 try writer.print(" {s}", .{symbol.value_ptr.define});
262 }
263 try writer.writeAll("\n");
264 }
265 }
266 if (self.null_terminate_string_table_strings) {
267 try writer.writeAll(" Strings in string tables will be null-terminated\n");
268 }
269 if (self.max_string_literal_codepoints != lex.default_max_string_literal_codepoints) {
270 try writer.print(" Max string literal length: {}\n", .{self.max_string_literal_codepoints});
271 }
272 if (self.silent_duplicate_control_ids) {
273 try writer.writeAll(" Duplicate control IDs will not emit warnings\n");
274 }
275 if (self.silent_duplicate_control_ids) {
276 try writer.writeAll(" Invalid code page in .rc will produce a warning (instead of an error)\n");
277 }
278
279 const language_id = self.default_language_id orelse res.Language.default;
280 const language_name = language_name: {
281 if (std.meta.intToEnum(lang.LanguageId, language_id)) |lang_enum_val| {
282 break :language_name @tagName(lang_enum_val);
283 } else |_| {}
284 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
285 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
286 }
287 break :language_name "<UNKNOWN>";
288 };
289 try writer.print("Default language: {s} (id=0x{x})\n", .{ language_name, language_id });
290
291 const code_page = self.default_code_page orelse .windows1252;
292 try writer.print("Default codepage: {s} (id={})\n", .{ @tagName(code_page), @intFromEnum(code_page) });
293 }
294};
295
296pub const Arg = struct {
297 prefix: enum { long, short, slash },
298 name_offset: usize,
299 full: []const u8,
300
301 pub fn fromString(str: []const u8) ?@This() {
302 if (std.mem.startsWith(u8, str, "--")) {
303 return .{ .prefix = .long, .name_offset = 2, .full = str };
304 } else if (std.mem.startsWith(u8, str, "-")) {
305 return .{ .prefix = .short, .name_offset = 1, .full = str };
306 } else if (std.mem.startsWith(u8, str, "/")) {
307 return .{ .prefix = .slash, .name_offset = 1, .full = str };
308 }
309 return null;
310 }
311
312 pub fn prefixSlice(self: Arg) []const u8 {
313 return self.full[0..(if (self.prefix == .long) 2 else 1)];
314 }
315
316 pub fn name(self: Arg) []const u8 {
317 return self.full[self.name_offset..];
318 }
319
320 pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 {
321 return self.name()[0..option_len];
322 }
323
324 pub fn missingSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
325 return .{
326 .point_at_next_arg = true,
327 .value_offset = 0,
328 .name_offset = self.name_offset,
329 .prefix_len = self.prefixSlice().len,
330 };
331 }
332
333 pub fn optionAndAfterSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
334 return self.optionSpan(0);
335 }
336
337 pub fn optionSpan(self: Arg, option_len: usize) Diagnostics.ErrorDetails.ArgSpan {
338 return .{
339 .name_offset = self.name_offset,
340 .prefix_len = self.prefixSlice().len,
341 .name_len = option_len,
342 };
343 }
344
345 pub const Value = struct {
346 slice: []const u8,
347 index_increment: u2 = 1,
348
349 pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan {
350 const prefix_len = arg.prefixSlice().len;
351 switch (self.index_increment) {
352 1 => return .{
353 .value_offset = @intFromPtr(self.slice.ptr) - @intFromPtr(arg.full.ptr),
354 .prefix_len = prefix_len,
355 .name_offset = arg.name_offset,
356 },
357 2 => return .{
358 .point_at_next_arg = true,
359 .prefix_len = prefix_len,
360 .name_offset = arg.name_offset,
361 },
362 else => unreachable,
363 }
364 }
365
366 pub fn index(self: Value, arg_index: usize) usize {
367 if (self.index_increment == 2) return arg_index + 1;
368 return arg_index;
369 }
370 };
371
372 pub fn value(self: Arg, option_len: usize, index: usize, args: []const []const u8) error{MissingValue}!Value {
373 const rest = self.full[self.name_offset + option_len ..];
374 if (rest.len > 0) return .{ .slice = rest };
375 if (index + 1 >= args.len) return error.MissingValue;
376 return .{ .slice = args[index + 1], .index_increment = 2 };
377 }
378
379 pub const Context = struct {
380 index: usize,
381 arg: Arg,
382 value: Value,
383 };
384};
385
386pub const ParseError = error{ParseError} || Allocator.Error;
387
388/// Note: Does not run `Options.maybeAppendRC` automatically. If that behavior is desired,
389/// it must be called separately.
390pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options {
391 var options = Options{ .allocator = allocator };
392 errdefer options.deinit();
393
394 var output_filename: ?[]const u8 = null;
395 var output_filename_context: Arg.Context = undefined;
396
397 var arg_i: usize = 1; // start at 1 to skip past the exe name
398 next_arg: while (arg_i < args.len) {
399 var arg = Arg.fromString(args[arg_i]) orelse break;
400 if (arg.name().len == 0) {
401 switch (arg.prefix) {
402 // -- on its own ends arg parsing
403 .long => {
404 arg_i += 1;
405 break;
406 },
407 // - or / on its own is an error
408 else => {
409 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
410 var msg_writer = err_details.msg.writer(allocator);
411 try msg_writer.print("invalid option: {s}", .{arg.prefixSlice()});
412 try diagnostics.append(err_details);
413 arg_i += 1;
414 continue :next_arg;
415 },
416 }
417 }
418
419 while (arg.name().len > 0) {
420 const arg_name = arg.name();
421 // Note: These cases should be in order from longest to shortest, since
422 // shorter options that are a substring of a longer one could make
423 // the longer option's branch unreachable.
424 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
425 options.preprocess = .no;
426 arg.name_offset += ":no-preprocess".len;
427 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
428 const value = arg.value(":auto-includes".len, arg_i, args) catch {
429 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
430 var msg_writer = err_details.msg.writer(allocator);
431 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":auto-includes".len) });
432 try diagnostics.append(err_details);
433 arg_i += 1;
434 break :next_arg;
435 };
436 options.auto_includes = std.meta.stringToEnum(Options.AutoIncludes, value.slice) orelse blk: {
437 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
438 var msg_writer = err_details.msg.writer(allocator);
439 try msg_writer.print("invalid auto includes setting: {s} ", .{value.slice});
440 try diagnostics.append(err_details);
441 break :blk options.auto_includes;
442 };
443 arg_i += value.index_increment;
444 continue :next_arg;
445 } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
446 // No-op, we don't display any 'logo' to suppress
447 arg.name_offset += "nologo".len;
448 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":debug")) {
449 options.debug = true;
450 arg.name_offset += ":debug".len;
451 }
452 // Unsupported LCX/LCE options that need a value (within the same arg only)
453 else if (std.ascii.startsWithIgnoreCase(arg_name, "tp:")) {
454 const rest = arg.full[arg.name_offset + 3 ..];
455 if (rest.len == 0) {
456 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = .{
457 .name_offset = arg.name_offset,
458 .prefix_len = arg.prefixSlice().len,
459 .value_offset = arg.name_offset + 3,
460 } };
461 var msg_writer = err_details.msg.writer(allocator);
462 try msg_writer.print("missing value for {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
463 try diagnostics.append(err_details);
464 }
465 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
466 var msg_writer = err_details.msg.writer(allocator);
467 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
468 try diagnostics.append(err_details);
469 arg_i += 1;
470 continue :next_arg;
471 }
472 // Unsupported LCX/LCE options that need a value
473 else if (std.ascii.startsWithIgnoreCase(arg_name, "tn")) {
474 const value = arg.value(2, arg_i, args) catch no_value: {
475 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
476 var msg_writer = err_details.msg.writer(allocator);
477 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
478 try diagnostics.append(err_details);
479 // dummy zero-length slice starting where the value would have been
480 const value_start = arg.name_offset + 2;
481 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
482 };
483 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
484 var msg_writer = err_details.msg.writer(allocator);
485 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
486 try diagnostics.append(err_details);
487 arg_i += value.index_increment;
488 continue :next_arg;
489 }
490 // Unsupported MUI options that need a value
491 else if (std.ascii.startsWithIgnoreCase(arg_name, "fm") or
492 std.ascii.startsWithIgnoreCase(arg_name, "gn") or
493 std.ascii.startsWithIgnoreCase(arg_name, "g2"))
494 {
495 const value = arg.value(2, arg_i, args) catch no_value: {
496 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
497 var msg_writer = err_details.msg.writer(allocator);
498 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
499 try diagnostics.append(err_details);
500 // dummy zero-length slice starting where the value would have been
501 const value_start = arg.name_offset + 2;
502 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
503 };
504 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
505 var msg_writer = err_details.msg.writer(allocator);
506 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
507 try diagnostics.append(err_details);
508 arg_i += value.index_increment;
509 continue :next_arg;
510 }
511 // Unsupported MUI options that do not need a value
512 else if (std.ascii.startsWithIgnoreCase(arg_name, "g1")) {
513 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
514 var msg_writer = err_details.msg.writer(allocator);
515 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
516 try diagnostics.append(err_details);
517 arg.name_offset += 2;
518 }
519 // Unsupported LCX/LCE options that do not need a value
520 else if (std.ascii.startsWithIgnoreCase(arg_name, "tm") or
521 std.ascii.startsWithIgnoreCase(arg_name, "tc") or
522 std.ascii.startsWithIgnoreCase(arg_name, "tw") or
523 std.ascii.startsWithIgnoreCase(arg_name, "te") or
524 std.ascii.startsWithIgnoreCase(arg_name, "ti") or
525 std.ascii.startsWithIgnoreCase(arg_name, "ta"))
526 {
527 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
528 var msg_writer = err_details.msg.writer(allocator);
529 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
530 try diagnostics.append(err_details);
531 arg.name_offset += 2;
532 } else if (std.ascii.startsWithIgnoreCase(arg_name, "fo")) {
533 const value = arg.value(2, arg_i, args) catch {
534 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
535 var msg_writer = err_details.msg.writer(allocator);
536 try msg_writer.print("missing output path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
537 try diagnostics.append(err_details);
538 arg_i += 1;
539 break :next_arg;
540 };
541 output_filename_context = .{ .index = arg_i, .arg = arg, .value = value };
542 output_filename = value.slice;
543 arg_i += value.index_increment;
544 continue :next_arg;
545 } else if (std.ascii.startsWithIgnoreCase(arg_name, "sl")) {
546 const value = arg.value(2, arg_i, args) catch {
547 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
548 var msg_writer = err_details.msg.writer(allocator);
549 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
550 try diagnostics.append(err_details);
551 arg_i += 1;
552 break :next_arg;
553 };
554 const percent_str = value.slice;
555 const percent: u32 = parsePercent(percent_str) catch {
556 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
557 var msg_writer = err_details.msg.writer(allocator);
558 try msg_writer.print("invalid percent format '{s}'", .{percent_str});
559 try diagnostics.append(err_details);
560 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
561 var note_writer = note_details.msg.writer(allocator);
562 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
563 try diagnostics.append(note_details);
564 arg_i += value.index_increment;
565 continue :next_arg;
566 };
567 if (percent == 0 or percent > 100) {
568 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
569 var msg_writer = err_details.msg.writer(allocator);
570 try msg_writer.print("percent out of range: {} (parsed from '{s}')", .{ percent, percent_str });
571 try diagnostics.append(err_details);
572 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
573 var note_writer = note_details.msg.writer(allocator);
574 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
575 try diagnostics.append(note_details);
576 arg_i += value.index_increment;
577 continue :next_arg;
578 }
579 const percent_float = @as(f32, @floatFromInt(percent)) / 100;
580 options.max_string_literal_codepoints = @intFromFloat(percent_float * max_string_literal_length_100_percent);
581 arg_i += value.index_increment;
582 continue :next_arg;
583 } else if (std.ascii.startsWithIgnoreCase(arg_name, "ln")) {
584 const value = arg.value(2, arg_i, args) catch {
585 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
586 var msg_writer = err_details.msg.writer(allocator);
587 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
588 try diagnostics.append(err_details);
589 arg_i += 1;
590 break :next_arg;
591 };
592 const tag = value.slice;
593 options.default_language_id = lang.tagToInt(tag) catch {
594 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
595 var msg_writer = err_details.msg.writer(allocator);
596 try msg_writer.print("invalid language tag: {s}", .{tag});
597 try diagnostics.append(err_details);
598 arg_i += value.index_increment;
599 continue :next_arg;
600 };
601 if (options.default_language_id.? == lang.LOCALE_CUSTOM_UNSPECIFIED) {
602 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
603 var msg_writer = err_details.msg.writer(allocator);
604 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 });
605 try diagnostics.append(err_details);
606 }
607 arg_i += value.index_increment;
608 continue :next_arg;
609 } else if (std.ascii.startsWithIgnoreCase(arg_name, "l")) {
610 const value = arg.value(1, arg_i, args) catch {
611 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
612 var msg_writer = err_details.msg.writer(allocator);
613 try msg_writer.print("missing language ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
614 try diagnostics.append(err_details);
615 arg_i += 1;
616 break :next_arg;
617 };
618 const num_str = value.slice;
619 options.default_language_id = lang.parseInt(num_str) catch {
620 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
621 var msg_writer = err_details.msg.writer(allocator);
622 try msg_writer.print("invalid language ID: {s}", .{num_str});
623 try diagnostics.append(err_details);
624 arg_i += value.index_increment;
625 continue :next_arg;
626 };
627 arg_i += value.index_increment;
628 continue :next_arg;
629 } else if (std.ascii.startsWithIgnoreCase(arg_name, "h") or std.mem.startsWith(u8, arg_name, "?")) {
630 options.print_help_and_exit = true;
631 // If there's been an error to this point, then we still want to fail
632 if (diagnostics.hasError()) return error.ParseError;
633 return options;
634 }
635 // 1 char unsupported MUI options that need a value
636 else if (std.ascii.startsWithIgnoreCase(arg_name, "q") or
637 std.ascii.startsWithIgnoreCase(arg_name, "g"))
638 {
639 const value = arg.value(1, arg_i, args) catch no_value: {
640 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
641 var msg_writer = err_details.msg.writer(allocator);
642 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
643 try diagnostics.append(err_details);
644 // dummy zero-length slice starting where the value would have been
645 const value_start = arg.name_offset + 1;
646 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
647 };
648 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
649 var msg_writer = err_details.msg.writer(allocator);
650 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
651 try diagnostics.append(err_details);
652 arg_i += value.index_increment;
653 continue :next_arg;
654 }
655 // Undocumented (and unsupported) options that need a value
656 // /z has to do something with font substitution
657 // /s has something to do with HWB resources being inserted into the .res
658 else if (std.ascii.startsWithIgnoreCase(arg_name, "z") or
659 std.ascii.startsWithIgnoreCase(arg_name, "s"))
660 {
661 const value = arg.value(1, arg_i, args) catch no_value: {
662 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
663 var msg_writer = err_details.msg.writer(allocator);
664 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
665 try diagnostics.append(err_details);
666 // dummy zero-length slice starting where the value would have been
667 const value_start = arg.name_offset + 1;
668 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
669 };
670 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
671 var msg_writer = err_details.msg.writer(allocator);
672 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
673 try diagnostics.append(err_details);
674 arg_i += value.index_increment;
675 continue :next_arg;
676 }
677 // 1 char unsupported LCX/LCE options that do not need a value
678 else if (std.ascii.startsWithIgnoreCase(arg_name, "t")) {
679 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
680 var msg_writer = err_details.msg.writer(allocator);
681 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
682 try diagnostics.append(err_details);
683 arg.name_offset += 1;
684 } else if (std.ascii.startsWithIgnoreCase(arg_name, "c")) {
685 const value = arg.value(1, arg_i, args) catch {
686 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
687 var msg_writer = err_details.msg.writer(allocator);
688 try msg_writer.print("missing code page ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
689 try diagnostics.append(err_details);
690 arg_i += 1;
691 break :next_arg;
692 };
693 const num_str = value.slice;
694 const code_page_id = std.fmt.parseUnsigned(u16, num_str, 10) catch {
695 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
696 var msg_writer = err_details.msg.writer(allocator);
697 try msg_writer.print("invalid code page ID: {s}", .{num_str});
698 try diagnostics.append(err_details);
699 arg_i += value.index_increment;
700 continue :next_arg;
701 };
702 options.default_code_page = CodePage.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {
703 error.InvalidCodePage => {
704 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
705 var msg_writer = err_details.msg.writer(allocator);
706 try msg_writer.print("invalid or unknown code page ID: {}", .{code_page_id});
707 try diagnostics.append(err_details);
708 arg_i += value.index_increment;
709 continue :next_arg;
710 },
711 error.UnsupportedCodePage => {
712 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
713 var msg_writer = err_details.msg.writer(allocator);
714 try msg_writer.print("unsupported code page: {s} (id={})", .{
715 @tagName(CodePage.getByIdentifier(code_page_id) catch unreachable),
716 code_page_id,
717 });
718 try diagnostics.append(err_details);
719 arg_i += value.index_increment;
720 continue :next_arg;
721 },
722 };
723 arg_i += value.index_increment;
724 continue :next_arg;
725 } else if (std.ascii.startsWithIgnoreCase(arg_name, "v")) {
726 options.verbose = true;
727 arg.name_offset += 1;
728 } else if (std.ascii.startsWithIgnoreCase(arg_name, "x")) {
729 options.ignore_include_env_var = true;
730 arg.name_offset += 1;
731 } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) {
732 options.preprocess = .only;
733 arg.name_offset += 1;
734 } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) {
735 const value = arg.value(1, arg_i, args) catch {
736 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
737 var msg_writer = err_details.msg.writer(allocator);
738 try msg_writer.print("missing include path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
739 try diagnostics.append(err_details);
740 arg_i += 1;
741 break :next_arg;
742 };
743 const path = value.slice;
744 const duped = try allocator.dupe(u8, path);
745 errdefer allocator.free(duped);
746 try options.extra_include_paths.append(options.allocator, duped);
747 arg_i += value.index_increment;
748 continue :next_arg;
749 } else if (std.ascii.startsWithIgnoreCase(arg_name, "r")) {
750 // From https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
751 // "Ignored. Provided for compatibility with existing makefiles."
752 arg.name_offset += 1;
753 } else if (std.ascii.startsWithIgnoreCase(arg_name, "n")) {
754 options.null_terminate_string_table_strings = true;
755 arg.name_offset += 1;
756 } else if (std.ascii.startsWithIgnoreCase(arg_name, "y")) {
757 options.silent_duplicate_control_ids = true;
758 arg.name_offset += 1;
759 } else if (std.ascii.startsWithIgnoreCase(arg_name, "w")) {
760 options.warn_instead_of_error_on_invalid_code_page = true;
761 arg.name_offset += 1;
762 } else if (std.ascii.startsWithIgnoreCase(arg_name, "a")) {
763 // Undocumented option with unknown function
764 // TODO: More investigation to figure out what it does (if anything)
765 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
766 var msg_writer = err_details.msg.writer(allocator);
767 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) });
768 try diagnostics.append(err_details);
769 arg.name_offset += 1;
770 } else if (std.ascii.startsWithIgnoreCase(arg_name, "d")) {
771 const value = arg.value(1, arg_i, args) catch {
772 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
773 var msg_writer = err_details.msg.writer(allocator);
774 try msg_writer.print("missing symbol to define after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
775 try diagnostics.append(err_details);
776 arg_i += 1;
777 break :next_arg;
778 };
779 var tokenizer = std.mem.tokenize(u8, value.slice, "=");
780 // guaranteed to exist since an empty value.slice would invoke
781 // the 'missing symbol to define' branch above
782 const symbol = tokenizer.next().?;
783 const symbol_value = tokenizer.next() orelse "1";
784
785 if (isValidIdentifier(symbol)) {
786 try options.define(symbol, symbol_value);
787 } else {
788 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
789 var msg_writer = err_details.msg.writer(allocator);
790 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be defined", .{symbol});
791 try diagnostics.append(err_details);
792 }
793 arg_i += value.index_increment;
794 continue :next_arg;
795 } else if (std.ascii.startsWithIgnoreCase(arg_name, "u")) {
796 const value = arg.value(1, arg_i, args) catch {
797 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
798 var msg_writer = err_details.msg.writer(allocator);
799 try msg_writer.print("missing symbol to undefine after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
800 try diagnostics.append(err_details);
801 arg_i += 1;
802 break :next_arg;
803 };
804 const symbol = value.slice;
805 if (isValidIdentifier(symbol)) {
806 try options.undefine(symbol);
807 } else {
808 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
809 var msg_writer = err_details.msg.writer(allocator);
810 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be undefined", .{symbol});
811 try diagnostics.append(err_details);
812 }
813 arg_i += value.index_increment;
814 continue :next_arg;
815 } else {
816 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
817 var msg_writer = err_details.msg.writer(allocator);
818 try msg_writer.print("invalid option: {s}{s}", .{ arg.prefixSlice(), arg.name() });
819 try diagnostics.append(err_details);
820 arg_i += 1;
821 continue :next_arg;
822 }
823 } else {
824 // The while loop exited via its conditional, meaning we are done with
825 // the current arg and can move on the the next
826 arg_i += 1;
827 continue;
828 }
829 }
830
831 const positionals = args[arg_i..];
832
833 if (positionals.len < 1) {
834 var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };
835 var msg_writer = err_details.msg.writer(allocator);
836 try msg_writer.writeAll("missing input filename");
837 try diagnostics.append(err_details);
838
839 const last_arg = args[args.len - 1];
840 if (arg_i > 1 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {
841 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
842 var note_writer = note_details.msg.writer(allocator);
843 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");
844 try diagnostics.append(note_details);
845 }
846
847 // This is a fatal enough problem to justify an early return, since
848 // things after this rely on the value of the input filename.
849 return error.ParseError;
850 }
851 options.input_filename = try allocator.dupe(u8, positionals[0]);
852
853 if (positionals.len > 1) {
854 if (output_filename != null) {
855 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i + 1 };
856 var msg_writer = err_details.msg.writer(allocator);
857 try msg_writer.writeAll("output filename already specified");
858 try diagnostics.append(err_details);
859 var note_details = Diagnostics.ErrorDetails{
860 .type = .note,
861 .arg_index = output_filename_context.value.index(output_filename_context.index),
862 .arg_span = output_filename_context.value.argSpan(output_filename_context.arg),
863 };
864 var note_writer = note_details.msg.writer(allocator);
865 try note_writer.writeAll("output filename previously specified here");
866 try diagnostics.append(note_details);
867 } else {
868 output_filename = positionals[1];
869 }
870 }
871 if (output_filename == null) {
872 var buf = std.ArrayList(u8).init(allocator);
873 errdefer buf.deinit();
874
875 if (std.fs.path.dirname(options.input_filename)) |dirname| {
876 var end_pos = dirname.len;
877 // We want to ensure that we write a path separator at the end, so if the dirname
878 // doesn't end with a path sep then include the char after the dirname
879 // which must be a path sep.
880 if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1;
881 try buf.appendSlice(options.input_filename[0..end_pos]);
882 }
883 try buf.appendSlice(std.fs.path.stem(options.input_filename));
884 if (options.preprocess == .only) {
885 try buf.appendSlice(".rcpp");
886 } else {
887 try buf.appendSlice(".res");
888 }
889
890 options.output_filename = try buf.toOwnedSlice();
891 } else {
892 options.output_filename = try allocator.dupe(u8, output_filename.?);
893 }
894
895 if (diagnostics.hasError()) {
896 return error.ParseError;
897 }
898
899 return options;
900}
901
902/// Returns true if the str is a valid C identifier for use in a #define/#undef macro
903pub fn isValidIdentifier(str: []const u8) bool {
904 for (str, 0..) |c, i| switch (c) {
905 '0'...'9' => if (i == 0) return false,
906 'a'...'z', 'A'...'Z', '_' => {},
907 else => return false,
908 };
909 return true;
910}
911
912/// This function is specific to how the Win32 RC command line interprets
913/// max string literal length percent.
914/// - Wraps on overflow of u32
915/// - Stops parsing on any invalid hexadecimal digits
916/// - Errors if a digit is not the first char
917/// - `-` (negative) prefix is allowed
918pub fn parsePercent(str: []const u8) error{InvalidFormat}!u32 {
919 var result: u32 = 0;
920 const radix: u8 = 10;
921 var buf = str;
922
923 const Prefix = enum { none, minus };
924 var prefix: Prefix = .none;
925 switch (buf[0]) {
926 '-' => {
927 prefix = .minus;
928 buf = buf[1..];
929 },
930 else => {},
931 }
932
933 for (buf, 0..) |c, i| {
934 const digit = switch (c) {
935 // On invalid digit for the radix, just stop parsing but don't fail
936 '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
937 else => {
938 // First digit must be valid
939 if (i == 0) {
940 return error.InvalidFormat;
941 }
942 break;
943 },
944 };
945
946 if (result != 0) {
947 result *%= radix;
948 }
949 result +%= digit;
950 }
951
952 switch (prefix) {
953 .none => {},
954 .minus => result = 0 -% result,
955 }
956
957 return result;
958}
959
960test parsePercent {
961 try std.testing.expectEqual(@as(u32, 16), try parsePercent("16"));
962 try std.testing.expectEqual(@as(u32, 0), try parsePercent("0x1A"));
963 try std.testing.expectEqual(@as(u32, 0x1), try parsePercent("1zzzz"));
964 try std.testing.expectEqual(@as(u32, 0xffffffff), try parsePercent("-1"));
965 try std.testing.expectEqual(@as(u32, 0xfffffff0), try parsePercent("-16"));
966 try std.testing.expectEqual(@as(u32, 1), try parsePercent("4294967297"));
967 try std.testing.expectError(error.InvalidFormat, parsePercent("--1"));
968 try std.testing.expectError(error.InvalidFormat, parsePercent("ha"));
969 try std.testing.expectError(error.InvalidFormat, parsePercent("¹"));
970 try std.testing.expectError(error.InvalidFormat, parsePercent("~1"));
971}
972
973pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, err_details: Diagnostics.ErrorDetails, args: []const []const u8) !void {
974 try config.setColor(writer, .dim);
975 try writer.writeAll("<cli>");
976 try config.setColor(writer, .reset);
977 try config.setColor(writer, .bold);
978 try writer.writeAll(": ");
979 switch (err_details.type) {
980 .err => {
981 try config.setColor(writer, .red);
982 try writer.writeAll("error: ");
983 },
984 .warning => {
985 try config.setColor(writer, .yellow);
986 try writer.writeAll("warning: ");
987 },
988 .note => {
989 try config.setColor(writer, .cyan);
990 try writer.writeAll("note: ");
991 },
992 }
993 try config.setColor(writer, .reset);
994 try config.setColor(writer, .bold);
995 try writer.writeAll(err_details.msg.items);
996 try writer.writeByte('\n');
997 try config.setColor(writer, .reset);
998
999 if (!err_details.print_args) {
1000 try writer.writeByte('\n');
1001 return;
1002 }
1003
1004 try config.setColor(writer, .dim);
1005 const prefix = " ... ";
1006 try writer.writeAll(prefix);
1007 try config.setColor(writer, .reset);
1008
1009 const arg_with_name = args[err_details.arg_index];
1010 const prefix_slice = arg_with_name[0..err_details.arg_span.prefix_len];
1011 const before_name_slice = arg_with_name[err_details.arg_span.prefix_len..err_details.arg_span.name_offset];
1012 var name_slice = arg_with_name[err_details.arg_span.name_offset..];
1013 if (err_details.arg_span.name_len > 0) name_slice.len = err_details.arg_span.name_len;
1014 const after_name_slice = arg_with_name[err_details.arg_span.name_offset + name_slice.len ..];
1015
1016 try writer.writeAll(prefix_slice);
1017 if (before_name_slice.len > 0) {
1018 try config.setColor(writer, .dim);
1019 try writer.writeAll(before_name_slice);
1020 try config.setColor(writer, .reset);
1021 }
1022 try writer.writeAll(name_slice);
1023 if (after_name_slice.len > 0) {
1024 try config.setColor(writer, .dim);
1025 try writer.writeAll(after_name_slice);
1026 try config.setColor(writer, .reset);
1027 }
1028
1029 var next_arg_len: usize = 0;
1030 if (err_details.arg_span.point_at_next_arg and err_details.arg_index + 1 < args.len) {
1031 const next_arg = args[err_details.arg_index + 1];
1032 try writer.writeByte(' ');
1033 try writer.writeAll(next_arg);
1034 next_arg_len = next_arg.len;
1035 }
1036
1037 const last_shown_arg_index = if (err_details.arg_span.point_at_next_arg) err_details.arg_index + 1 else err_details.arg_index;
1038 if (last_shown_arg_index + 1 < args.len) {
1039 // special case for when pointing to a missing value within the same arg
1040 // as the name
1041 if (err_details.arg_span.value_offset >= arg_with_name.len) {
1042 try writer.writeByte(' ');
1043 }
1044 try config.setColor(writer, .dim);
1045 try writer.writeAll(" ...");
1046 try config.setColor(writer, .reset);
1047 }
1048 try writer.writeByte('\n');
1049
1050 try config.setColor(writer, .green);
1051 try writer.writeByteNTimes(' ', prefix.len);
1052 // Special case for when the option is *only* a prefix (e.g. invalid option: -)
1053 if (err_details.arg_span.prefix_len == arg_with_name.len) {
1054 try writer.writeByteNTimes('^', err_details.arg_span.prefix_len);
1055 } else {
1056 try writer.writeByteNTimes('~', err_details.arg_span.prefix_len);
1057 try writer.writeByteNTimes(' ', err_details.arg_span.name_offset - err_details.arg_span.prefix_len);
1058 if (!err_details.arg_span.point_at_next_arg and err_details.arg_span.value_offset == 0) {
1059 try writer.writeByte('^');
1060 try writer.writeByteNTimes('~', name_slice.len - 1);
1061 } else if (err_details.arg_span.value_offset > 0) {
1062 try writer.writeByteNTimes('~', err_details.arg_span.value_offset - err_details.arg_span.name_offset);
1063 try writer.writeByte('^');
1064 if (err_details.arg_span.value_offset < arg_with_name.len) {
1065 try writer.writeByteNTimes('~', arg_with_name.len - err_details.arg_span.value_offset - 1);
1066 }
1067 } else if (err_details.arg_span.point_at_next_arg) {
1068 try writer.writeByteNTimes('~', arg_with_name.len - err_details.arg_span.name_offset + 1);
1069 try writer.writeByte('^');
1070 if (next_arg_len > 0) {
1071 try writer.writeByteNTimes('~', next_arg_len - 1);
1072 }
1073 }
1074 }
1075 try writer.writeByte('\n');
1076 try config.setColor(writer, .reset);
1077}
1078
1079fn testParse(args: []const []const u8) !Options {
1080 return (try testParseOutput(args, "")).?;
1081}
1082
1083fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options {
1084 return (try testParseOutput(args, expected_output)).?;
1085}
1086
1087fn testParseError(args: []const []const u8, expected_output: []const u8) !void {
1088 var maybe_options = try testParseOutput(args, expected_output);
1089 if (maybe_options != null) {
1090 std.debug.print("expected error, got options: {}\n", .{maybe_options.?});
1091 maybe_options.?.deinit();
1092 return error.TestExpectedError;
1093 }
1094}
1095
1096fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Options {
1097 var diagnostics = Diagnostics.init(std.testing.allocator);
1098 defer diagnostics.deinit();
1099
1100 var output = std.ArrayList(u8).init(std.testing.allocator);
1101 defer output.deinit();
1102
1103 var options = parse(std.testing.allocator, args, &diagnostics) catch |err| switch (err) {
1104 error.ParseError => {
1105 try diagnostics.renderToWriter(args, output.writer(), .no_color);
1106 try std.testing.expectEqualStrings(expected_output, output.items);
1107 return null;
1108 },
1109 else => |e| return e,
1110 };
1111 errdefer options.deinit();
1112
1113 try diagnostics.renderToWriter(args, output.writer(), .no_color);
1114 try std.testing.expectEqualStrings(expected_output, output.items);
1115 return options;
1116}
1117
1118test "parse errors: basic" {
1119 try testParseError(&.{ "foo.exe", "/" },
1120 \\<cli>: error: invalid option: /
1121 \\ ... /
1122 \\ ^
1123 \\<cli>: error: missing input filename
1124 \\
1125 \\
1126 );
1127 try testParseError(&.{ "foo.exe", "/ln" },
1128 \\<cli>: error: missing language tag after /ln option
1129 \\ ... /ln
1130 \\ ~~~~^
1131 \\<cli>: error: missing input filename
1132 \\
1133 \\
1134 );
1135 try testParseError(&.{ "foo.exe", "-vln" },
1136 \\<cli>: error: missing language tag after -ln option
1137 \\ ... -vln
1138 \\ ~ ~~~^
1139 \\<cli>: error: missing input filename
1140 \\
1141 \\
1142 );
1143 try testParseError(&.{ "foo.exe", "/_not-an-option" },
1144 \\<cli>: error: invalid option: /_not-an-option
1145 \\ ... /_not-an-option
1146 \\ ~^~~~~~~~~~~~~~
1147 \\<cli>: error: missing input filename
1148 \\
1149 \\
1150 );
1151 try testParseError(&.{ "foo.exe", "-_not-an-option" },
1152 \\<cli>: error: invalid option: -_not-an-option
1153 \\ ... -_not-an-option
1154 \\ ~^~~~~~~~~~~~~~
1155 \\<cli>: error: missing input filename
1156 \\
1157 \\
1158 );
1159 try testParseError(&.{ "foo.exe", "--_not-an-option" },
1160 \\<cli>: error: invalid option: --_not-an-option
1161 \\ ... --_not-an-option
1162 \\ ~~^~~~~~~~~~~~~~
1163 \\<cli>: error: missing input filename
1164 \\
1165 \\
1166 );
1167 try testParseError(&.{ "foo.exe", "/v_not-an-option" },
1168 \\<cli>: error: invalid option: /_not-an-option
1169 \\ ... /v_not-an-option
1170 \\ ~ ^~~~~~~~~~~~~~
1171 \\<cli>: error: missing input filename
1172 \\
1173 \\
1174 );
1175 try testParseError(&.{ "foo.exe", "-v_not-an-option" },
1176 \\<cli>: error: invalid option: -_not-an-option
1177 \\ ... -v_not-an-option
1178 \\ ~ ^~~~~~~~~~~~~~
1179 \\<cli>: error: missing input filename
1180 \\
1181 \\
1182 );
1183 try testParseError(&.{ "foo.exe", "--v_not-an-option" },
1184 \\<cli>: error: invalid option: --_not-an-option
1185 \\ ... --v_not-an-option
1186 \\ ~~ ^~~~~~~~~~~~~~
1187 \\<cli>: error: missing input filename
1188 \\
1189 \\
1190 );
1191 try testParseError(&.{ "foo.exe", "/some/absolute/path/parsed/as/an/option.rc" },
1192 \\<cli>: error: the /s option is unsupported
1193 \\ ... /some/absolute/path/parsed/as/an/option.rc
1194 \\ ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1195 \\<cli>: error: missing input filename
1196 \\
1197 \\<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
1198 \\ ... /some/absolute/path/parsed/as/an/option.rc
1199 \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1200 \\
1201 );
1202}
1203
1204test "parse errors: /ln" {
1205 try testParseError(&.{ "foo.exe", "/ln", "invalid", "foo.rc" },
1206 \\<cli>: error: invalid language tag: invalid
1207 \\ ... /ln invalid ...
1208 \\ ~~~~^~~~~~~
1209 \\
1210 );
1211 try testParseError(&.{ "foo.exe", "/lninvalid", "foo.rc" },
1212 \\<cli>: error: invalid language tag: invalid
1213 \\ ... /lninvalid ...
1214 \\ ~~~^~~~~~~
1215 \\
1216 );
1217}
1218
1219test "parse: options" {
1220 {
1221 var options = try testParse(&.{ "foo.exe", "/v", "foo.rc" });
1222 defer options.deinit();
1223
1224 try std.testing.expectEqual(true, options.verbose);
1225 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1226 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1227 }
1228 {
1229 var options = try testParse(&.{ "foo.exe", "/vx", "foo.rc" });
1230 defer options.deinit();
1231
1232 try std.testing.expectEqual(true, options.verbose);
1233 try std.testing.expectEqual(true, options.ignore_include_env_var);
1234 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1235 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1236 }
1237 {
1238 var options = try testParse(&.{ "foo.exe", "/xv", "foo.rc" });
1239 defer options.deinit();
1240
1241 try std.testing.expectEqual(true, options.verbose);
1242 try std.testing.expectEqual(true, options.ignore_include_env_var);
1243 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1244 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1245 }
1246 {
1247 var options = try testParse(&.{ "foo.exe", "/xvFObar.res", "foo.rc" });
1248 defer options.deinit();
1249
1250 try std.testing.expectEqual(true, options.verbose);
1251 try std.testing.expectEqual(true, options.ignore_include_env_var);
1252 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1253 try std.testing.expectEqualStrings("bar.res", options.output_filename);
1254 }
1255}
1256
1257test "parse: define and undefine" {
1258 {
1259 var options = try testParse(&.{ "foo.exe", "/dfoo", "foo.rc" });
1260 defer options.deinit();
1261
1262 const action = options.symbols.get("foo").?;
1263 try std.testing.expectEqual(Options.SymbolAction.define, action);
1264 try std.testing.expectEqualStrings("1", action.define);
1265 }
1266 {
1267 var options = try testParse(&.{ "foo.exe", "/dfoo=bar", "/dfoo=baz", "foo.rc" });
1268 defer options.deinit();
1269
1270 const action = options.symbols.get("foo").?;
1271 try std.testing.expectEqual(Options.SymbolAction.define, action);
1272 try std.testing.expectEqualStrings("baz", action.define);
1273 }
1274 {
1275 var options = try testParse(&.{ "foo.exe", "/ufoo", "foo.rc" });
1276 defer options.deinit();
1277
1278 const action = options.symbols.get("foo").?;
1279 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1280 }
1281 {
1282 // Once undefined, future defines are ignored
1283 var options = try testParse(&.{ "foo.exe", "/ufoo", "/dfoo", "foo.rc" });
1284 defer options.deinit();
1285
1286 const action = options.symbols.get("foo").?;
1287 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1288 }
1289 {
1290 // Undefined always takes precedence
1291 var options = try testParse(&.{ "foo.exe", "/dfoo", "/ufoo", "/dfoo", "foo.rc" });
1292 defer options.deinit();
1293
1294 const action = options.symbols.get("foo").?;
1295 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1296 }
1297 {
1298 // Warn + ignore invalid identifiers
1299 var options = try testParseWarning(
1300 &.{ "foo.exe", "/dfoo bar", "/u", "0leadingdigit", "foo.rc" },
1301 \\<cli>: warning: symbol "foo bar" is not a valid identifier and therefore cannot be defined
1302 \\ ... /dfoo bar ...
1303 \\ ~~^~~~~~~
1304 \\<cli>: warning: symbol "0leadingdigit" is not a valid identifier and therefore cannot be undefined
1305 \\ ... /u 0leadingdigit ...
1306 \\ ~~~^~~~~~~~~~~~~
1307 \\
1308 ,
1309 );
1310 defer options.deinit();
1311
1312 try std.testing.expectEqual(@as(usize, 0), options.symbols.count());
1313 }
1314}
1315
1316test "parse: /sl" {
1317 try testParseError(&.{ "foo.exe", "/sl", "0", "foo.rc" },
1318 \\<cli>: error: percent out of range: 0 (parsed from '0')
1319 \\ ... /sl 0 ...
1320 \\ ~~~~^
1321 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1322 \\
1323 \\
1324 );
1325 try testParseError(&.{ "foo.exe", "/sl", "abcd", "foo.rc" },
1326 \\<cli>: error: invalid percent format 'abcd'
1327 \\ ... /sl abcd ...
1328 \\ ~~~~^~~~
1329 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1330 \\
1331 \\
1332 );
1333 {
1334 var options = try testParse(&.{ "foo.exe", "foo.rc" });
1335 defer options.deinit();
1336
1337 try std.testing.expectEqual(@as(u15, lex.default_max_string_literal_codepoints), options.max_string_literal_codepoints);
1338 }
1339 {
1340 var options = try testParse(&.{ "foo.exe", "/sl100", "foo.rc" });
1341 defer options.deinit();
1342
1343 try std.testing.expectEqual(@as(u15, max_string_literal_length_100_percent), options.max_string_literal_codepoints);
1344 }
1345 {
1346 var options = try testParse(&.{ "foo.exe", "-SL33", "foo.rc" });
1347 defer options.deinit();
1348
1349 try std.testing.expectEqual(@as(u15, 2703), options.max_string_literal_codepoints);
1350 }
1351 {
1352 var options = try testParse(&.{ "foo.exe", "/sl15", "foo.rc" });
1353 defer options.deinit();
1354
1355 try std.testing.expectEqual(@as(u15, 1228), options.max_string_literal_codepoints);
1356 }
1357}
1358
1359test "parse: unsupported MUI-related options" {
1360 try testParseError(&.{ "foo.exe", "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" },
1361 \\<cli>: error: the /q option is unsupported
1362 \\ ... /q ...
1363 \\ ~^
1364 \\<cli>: error: the /g1 option is unsupported
1365 \\ ... /g1 ...
1366 \\ ~^~
1367 \\<cli>: error: the -G2 option is unsupported
1368 \\ ... -G2 ...
1369 \\ ~^~
1370 \\<cli>: error: the /fm option is unsupported
1371 \\ ... /fm ...
1372 \\ ~^~
1373 \\<cli>: error: the /g option is unsupported
1374 \\ ... /g ...
1375 \\ ~^
1376 \\
1377 );
1378}
1379
1380test "parse: unsupported LCX/LCE-related options" {
1381 try testParseError(&.{ "foo.exe", "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" },
1382 \\<cli>: error: the /t option is unsupported
1383 \\ ... /t ...
1384 \\ ~^
1385 \\<cli>: error: missing value for /tp: option
1386 \\ ... /tp: ...
1387 \\ ~~~~^
1388 \\<cli>: error: the /tp: option is unsupported
1389 \\ ... /tp: ...
1390 \\ ~^~~
1391 \\<cli>: error: the /tp: option is unsupported
1392 \\ ... /tp:blah ...
1393 \\ ~^~~~~~~
1394 \\<cli>: error: the /tm option is unsupported
1395 \\ ... /tm ...
1396 \\ ~^~
1397 \\<cli>: error: the /tc option is unsupported
1398 \\ ... /tc ...
1399 \\ ~^~
1400 \\<cli>: error: the /tw option is unsupported
1401 \\ ... /tw ...
1402 \\ ~^~
1403 \\<cli>: error: the -TE option is unsupported
1404 \\ ... -TEti ...
1405 \\ ~^~
1406 \\<cli>: error: the -ti option is unsupported
1407 \\ ... -TEti ...
1408 \\ ~ ^~
1409 \\<cli>: error: the /ta option is unsupported
1410 \\ ... /ta ...
1411 \\ ~^~
1412 \\<cli>: error: the /tn option is unsupported
1413 \\ ... /tn ...
1414 \\ ~^~
1415 \\
1416 );
1417}
1418
1419test "maybeAppendRC" {
1420 var tmp = std.testing.tmpDir(.{});
1421 defer tmp.cleanup();
1422
1423 var options = try testParse(&.{ "foo.exe", "foo" });
1424 defer options.deinit();
1425 try std.testing.expectEqualStrings("foo", options.input_filename);
1426
1427 // Create the file so that it's found. In this scenario, .rc should not get
1428 // appended.
1429 var file = try tmp.dir.createFile("foo", .{});
1430 file.close();
1431 try options.maybeAppendRC(tmp.dir);
1432 try std.testing.expectEqualStrings("foo", options.input_filename);
1433
1434 // Now delete the file and try again. Since the verbatim name is no longer found
1435 // and the input filename does not have an extension, .rc should get appended.
1436 try tmp.dir.deleteFile("foo");
1437 try options.maybeAppendRC(tmp.dir);
1438 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1439}
src/resinator/code_pages.zig deleted-487
...@@ -1,487 +0,0 @@
1const std = @import("std");
2const windows1252 = @import("windows1252.zig");
3
4// TODO: Parts of this comment block may be more relevant to string/NameOrOrdinal parsing
5// than it is to the stuff in this file.
6//
7// ‰ representations for context:
8// Win-1252 89
9// UTF-8 E2 80 B0
10// UTF-16 20 30
11//
12// With code page 65001:
13// ‰ RCDATA { "‰" L"‰" }
14// File encoded as Windows-1252:
15// ‰ => <U+FFFD REPLACEMENT CHARACTER> as u16
16// "‰" => 0x3F ('?')
17// L"‰" => <U+FFFD REPLACEMENT CHARACTER> as u16
18// File encoded as UTF-8:
19// ‰ => <U+2030 ‰> as u16
20// "‰" => 0x89 ('‰' encoded as Windows-1252)
21// L"‰" => <U+2030 ‰> as u16
22//
23// With code page 1252:
24// ‰ RCDATA { "‰" L"‰" }
25// File encoded as Windows-1252:
26// ‰ => <U+2030 ‰> as u16
27// "‰" => 0x89 ('‰' encoded as Windows-1252)
28// L"‰" => <U+2030 ‰> as u16
29// File encoded as UTF-8:
30// ‰ => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16
31// ^ first byte of utf8 representation
32// ^ second byte of UTF-8 representation (0x80), but interpretted as
33// Windows-1252 ('€') and then converted to UTF-16 (<U+20AC>)
34// ^ third byte of utf8 representation
35// "‰" => 0xE2, 0x80, 0xB0 (the bytes of the UTF-8 representation)
36// L"‰" => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 (see '‰ =>' explanation)
37//
38// With code page 1252:
39// <0x90> RCDATA { "<0x90>" L"<0x90>" }
40// File encoded as Windows-1252:
41// <0x90> => 0x90 as u16
42// "<0x90>" => 0x90
43// L"<0x90>" => 0x90 as u16
44// File encoded as UTF-8:
45// <0x90> => 0xC2 as u16, 0x90 as u16
46// "<0x90>" => 0xC2, 0x90 (the bytes of the UTF-8 representation of <U+0090>)
47// L"<0x90>" => 0xC2 as u16, 0x90 as u16
48//
49// Within a raw data block, file encoded as Windows-1252 (Â is <0xC2>):
50// "Âa" L"Âa" "\xC2ad" L"\xC2AD"
51// With code page 1252:
52// C2 61 C2 00 61 00 C2 61 64 AD C2
53// Â^ a^ Â~~~^ a~~~^ .^ a^ d^ ^~~~~\xC2AD
54// \xC2~`
55// With code page 65001:
56// 3F 61 FD FF 61 00 C2 61 64 AD C2
57// ^. a^ ^~~~. a~~~^ ^. a^ d^ ^~~~~\xC2AD
58// `. `. `~\xC2
59// `. `.~<0xC2>a is not well-formed UTF-8 (0xC2 expects a continutation byte after it).
60// `. Because 'a' is a valid first byte of a UTF-8 sequence, it is not included in the
61// `. invalid sequence so only the <0xC2> gets converted to <U+FFFD>.
62// `~Same as ^ but converted to '?' instead.
63//
64// Within a raw data block, file encoded as Windows-1252 (ð is <0xF0>, € is <0x80>):
65// "ð€a" L"ð€a"
66// With code page 1252:
67// F0 80 61 F0 00 AC 20 61 00
68// ð^ €^ a^ ð~~~^ €~~~^ a~~~^
69// With code page 65001:
70// 3F 61 FD FF 61 00
71// ^. a^ ^~~~. a~~~^
72// `. `.
73// `. `.~<0xF0><0x80> is not well-formed UTF-8, and <0x80> is not a valid first byte, so
74// `. both bytes are considered an invalid sequence and get converted to '<U+FFFD>'
75// `~Same as ^ but converted to '?' instead.
76
77/// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
78pub const CodePage = enum(u16) {
79 // supported
80 windows1252 = 1252, // windows-1252 ANSI Latin 1; Western European (Windows)
81 utf8 = 65001, // utf-8 Unicode (UTF-8)
82
83 // unsupported but valid
84 ibm037 = 37, // IBM037 IBM EBCDIC US-Canada
85 ibm437 = 437, // IBM437 OEM United States
86 ibm500 = 500, // IBM500 IBM EBCDIC International
87 asmo708 = 708, // ASMO-708 Arabic (ASMO 708)
88 asmo449plus = 709, // Arabic (ASMO-449+, BCON V4)
89 transparent_arabic = 710, // Arabic - Transparent Arabic
90 dos720 = 720, // DOS-720 Arabic (Transparent ASMO); Arabic (DOS)
91 ibm737 = 737, // ibm737 OEM Greek (formerly 437G); Greek (DOS)
92 ibm775 = 775, // ibm775 OEM Baltic; Baltic (DOS)
93 ibm850 = 850, // ibm850 OEM Multilingual Latin 1; Western European (DOS)
94 ibm852 = 852, // ibm852 OEM Latin 2; Central European (DOS)
95 ibm855 = 855, // IBM855 OEM Cyrillic (primarily Russian)
96 ibm857 = 857, // ibm857 OEM Turkish; Turkish (DOS)
97 ibm00858 = 858, // IBM00858 OEM Multilingual Latin 1 + Euro symbol
98 ibm860 = 860, // IBM860 OEM Portuguese; Portuguese (DOS)
99 ibm861 = 861, // ibm861 OEM Icelandic; Icelandic (DOS)
100 dos862 = 862, // DOS-862 OEM Hebrew; Hebrew (DOS)
101 ibm863 = 863, // IBM863 OEM French Canadian; French Canadian (DOS)
102 ibm864 = 864, // IBM864 OEM Arabic; Arabic (864)
103 ibm865 = 865, // IBM865 OEM Nordic; Nordic (DOS)
104 cp866 = 866, // cp866 OEM Russian; Cyrillic (DOS)
105 ibm869 = 869, // ibm869 OEM Modern Greek; Greek, Modern (DOS)
106 ibm870 = 870, // IBM870 IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2
107 windows874 = 874, // windows-874 Thai (Windows)
108 cp875 = 875, // cp875 IBM EBCDIC Greek Modern
109 shift_jis = 932, // shift_jis ANSI/OEM Japanese; Japanese (Shift-JIS)
110 gb2312 = 936, // gb2312 ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)
111 ks_c_5601_1987 = 949, // ks_c_5601-1987 ANSI/OEM Korean (Unified Hangul Code)
112 big5 = 950, // big5 ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5)
113 ibm1026 = 1026, // IBM1026 IBM EBCDIC Turkish (Latin 5)
114 ibm01047 = 1047, // IBM01047 IBM EBCDIC Latin 1/Open System
115 ibm01140 = 1140, // IBM01140 IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro)
116 ibm01141 = 1141, // IBM01141 IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro)
117 ibm01142 = 1142, // IBM01142 IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro)
118 ibm01143 = 1143, // IBM01143 IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro)
119 ibm01144 = 1144, // IBM01144 IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro)
120 ibm01145 = 1145, // IBM01145 IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro)
121 ibm01146 = 1146, // IBM01146 IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro)
122 ibm01147 = 1147, // IBM01147 IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro)
123 ibm01148 = 1148, // IBM01148 IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro)
124 ibm01149 = 1149, // IBM01149 IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro)
125 utf16 = 1200, // utf-16 Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications
126 utf16_fffe = 1201, // unicodeFFFE Unicode UTF-16, big endian byte order; available only to managed applications
127 windows1250 = 1250, // windows-1250 ANSI Central European; Central European (Windows)
128 windows1251 = 1251, // windows-1251 ANSI Cyrillic; Cyrillic (Windows)
129 windows1253 = 1253, // windows-1253 ANSI Greek; Greek (Windows)
130 windows1254 = 1254, // windows-1254 ANSI Turkish; Turkish (Windows)
131 windows1255 = 1255, // windows-1255 ANSI Hebrew; Hebrew (Windows)
132 windows1256 = 1256, // windows-1256 ANSI Arabic; Arabic (Windows)
133 windows1257 = 1257, // windows-1257 ANSI Baltic; Baltic (Windows)
134 windows1258 = 1258, // windows-1258 ANSI/OEM Vietnamese; Vietnamese (Windows)
135 johab = 1361, // Johab Korean (Johab)
136 macintosh = 10000, // macintosh MAC Roman; Western European (Mac)
137 x_mac_japanese = 10001, // x-mac-japanese Japanese (Mac)
138 x_mac_chinesetrad = 10002, // x-mac-chinesetrad MAC Traditional Chinese (Big5); Chinese Traditional (Mac)
139 x_mac_korean = 10003, // x-mac-korean Korean (Mac)
140 x_mac_arabic = 10004, // x-mac-arabic Arabic (Mac)
141 x_mac_hebrew = 10005, // x-mac-hebrew Hebrew (Mac)
142 x_mac_greek = 10006, // x-mac-greek Greek (Mac)
143 x_mac_cyrillic = 10007, // x-mac-cyrillic Cyrillic (Mac)
144 x_mac_chinesesimp = 10008, // x-mac-chinesesimp MAC Simplified Chinese (GB 2312); Chinese Simplified (Mac)
145 x_mac_romanian = 10010, // x-mac-romanian Romanian (Mac)
146 x_mac_ukranian = 10017, // x-mac-ukrainian Ukrainian (Mac)
147 x_mac_thai = 10021, // x-mac-thai Thai (Mac)
148 x_mac_ce = 10029, // x-mac-ce MAC Latin 2; Central European (Mac)
149 x_mac_icelandic = 10079, // x-mac-icelandic Icelandic (Mac)
150 x_mac_turkish = 10081, // x-mac-turkish Turkish (Mac)
151 x_mac_croatian = 10082, // x-mac-croatian Croatian (Mac)
152 utf32 = 12000, // utf-32 Unicode UTF-32, little endian byte order; available only to managed applications
153 utf32_be = 12001, // utf-32BE Unicode UTF-32, big endian byte order; available only to managed applications
154 x_chinese_cns = 20000, // x-Chinese_CNS CNS Taiwan; Chinese Traditional (CNS)
155 x_cp20001 = 20001, // x-cp20001 TCA Taiwan
156 x_chinese_eten = 20002, // x_Chinese-Eten Eten Taiwan; Chinese Traditional (Eten)
157 x_cp20003 = 20003, // x-cp20003 IBM5550 Taiwan
158 x_cp20004 = 20004, // x-cp20004 TeleText Taiwan
159 x_cp20005 = 20005, // x-cp20005 Wang Taiwan
160 x_ia5 = 20105, // x-IA5 IA5 (IRV International Alphabet No. 5, 7-bit); Western European (IA5)
161 x_ia5_german = 20106, // x-IA5-German IA5 German (7-bit)
162 x_ia5_swedish = 20107, // x-IA5-Swedish IA5 Swedish (7-bit)
163 x_ia5_norwegian = 20108, // x-IA5-Norwegian IA5 Norwegian (7-bit)
164 us_ascii = 20127, // us-ascii US-ASCII (7-bit)
165 x_cp20261 = 20261, // x-cp20261 T.61
166 x_cp20269 = 20269, // x-cp20269 ISO 6937 Non-Spacing Accent
167 ibm273 = 20273, // IBM273 IBM EBCDIC Germany
168 ibm277 = 20277, // IBM277 IBM EBCDIC Denmark-Norway
169 ibm278 = 20278, // IBM278 IBM EBCDIC Finland-Sweden
170 ibm280 = 20280, // IBM280 IBM EBCDIC Italy
171 ibm284 = 20284, // IBM284 IBM EBCDIC Latin America-Spain
172 ibm285 = 20285, // IBM285 IBM EBCDIC United Kingdom
173 ibm290 = 20290, // IBM290 IBM EBCDIC Japanese Katakana Extended
174 ibm297 = 20297, // IBM297 IBM EBCDIC France
175 ibm420 = 20420, // IBM420 IBM EBCDIC Arabic
176 ibm423 = 20423, // IBM423 IBM EBCDIC Greek
177 ibm424 = 20424, // IBM424 IBM EBCDIC Hebrew
178 x_ebcdic_korean_extended = 20833, // x-EBCDIC-KoreanExtended IBM EBCDIC Korean Extended
179 ibm_thai = 20838, // IBM-Thai IBM EBCDIC Thai
180 koi8_r = 20866, // koi8-r Russian (KOI8-R); Cyrillic (KOI8-R)
181 ibm871 = 20871, // IBM871 IBM EBCDIC Icelandic
182 ibm880 = 20880, // IBM880 IBM EBCDIC Cyrillic Russian
183 ibm905 = 20905, // IBM905 IBM EBCDIC Turkish
184 ibm00924 = 20924, // IBM00924 IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)
185 euc_jp_jis = 20932, // EUC-JP Japanese (JIS 0208-1990 and 0212-1990)
186 x_cp20936 = 20936, // x-cp20936 Simplified Chinese (GB2312); Chinese Simplified (GB2312-80)
187 x_cp20949 = 20949, // x-cp20949 Korean Wansung
188 cp1025 = 21025, // cp1025 IBM EBCDIC Cyrillic Serbian-Bulgarian
189 // = 21027, // (deprecated)
190 koi8_u = 21866, // koi8-u Ukrainian (KOI8-U); Cyrillic (KOI8-U)
191 iso8859_1 = 28591, // iso-8859-1 ISO 8859-1 Latin 1; Western European (ISO)
192 iso8859_2 = 28592, // iso-8859-2 ISO 8859-2 Central European; Central European (ISO)
193 iso8859_3 = 28593, // iso-8859-3 ISO 8859-3 Latin 3
194 iso8859_4 = 28594, // iso-8859-4 ISO 8859-4 Baltic
195 iso8859_5 = 28595, // iso-8859-5 ISO 8859-5 Cyrillic
196 iso8859_6 = 28596, // iso-8859-6 ISO 8859-6 Arabic
197 iso8859_7 = 28597, // iso-8859-7 ISO 8859-7 Greek
198 iso8859_8 = 28598, // iso-8859-8 ISO 8859-8 Hebrew; Hebrew (ISO-Visual)
199 iso8859_9 = 28599, // iso-8859-9 ISO 8859-9 Turkish
200 iso8859_13 = 28603, // iso-8859-13 ISO 8859-13 Estonian
201 iso8859_15 = 28605, // iso-8859-15 ISO 8859-15 Latin 9
202 x_europa = 29001, // x-Europa Europa 3
203 is8859_8_i = 38598, // iso-8859-8-i ISO 8859-8 Hebrew; Hebrew (ISO-Logical)
204 iso2022_jp = 50220, // iso-2022-jp ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS)
205 cs_iso2022_jp = 50221, // csISO2022JP ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana)
206 iso2022_jp_jis_x = 50222, // iso-2022-jp ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI)
207 iso2022_kr = 50225, // iso-2022-kr ISO 2022 Korean
208 x_cp50227 = 50227, // x-cp50227 ISO 2022 Simplified Chinese; Chinese Simplified (ISO 2022)
209 iso2022_chinesetrad = 50229, // ISO 2022 Traditional Chinese
210 ebcdic_jp_katakana_extended = 50930, // EBCDIC Japanese (Katakana) Extended
211 ebcdic_us_ca_jp = 50931, // EBCDIC US-Canada and Japanese
212 ebcdic_kr_extended = 50933, // EBCDIC Korean Extended and Korean
213 ebcdic_chinesesimp_extended = 50935, // EBCDIC Simplified Chinese Extended and Simplified Chinese
214 ebcdic_chinesesimp = 50936, // EBCDIC Simplified Chinese
215 ebcdic_us_ca_chinesetrad = 50937, // EBCDIC US-Canada and Traditional Chinese
216 ebcdic_jp_latin_extended = 50939, // EBCDIC Japanese (Latin) Extended and Japanese
217 euc_jp = 51932, // euc-jp EUC Japanese
218 euc_cn = 51936, // EUC-CN EUC Simplified Chinese; Chinese Simplified (EUC)
219 euc_kr = 51949, // euc-kr EUC Korean
220 euc_chinesetrad = 51950, // EUC Traditional Chinese
221 hz_gb2312 = 52936, // hz-gb-2312 HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)
222 gb18030 = 54936, // GB18030 Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030)
223 x_iscii_de = 57002, // x-iscii-de ISCII Devanagari
224 x_iscii_be = 57003, // x-iscii-be ISCII Bangla
225 x_iscii_ta = 57004, // x-iscii-ta ISCII Tamil
226 x_iscii_te = 57005, // x-iscii-te ISCII Telugu
227 x_iscii_as = 57006, // x-iscii-as ISCII Assamese
228 x_iscii_or = 57007, // x-iscii-or ISCII Odia
229 x_iscii_ka = 57008, // x-iscii-ka ISCII Kannada
230 x_iscii_ma = 57009, // x-iscii-ma ISCII Malayalam
231 x_iscii_gu = 57010, // x-iscii-gu ISCII Gujarati
232 x_iscii_pa = 57011, // x-iscii-pa ISCII Punjabi
233 utf7 = 65000, // utf-7 Unicode (UTF-7)
234
235 pub fn codepointAt(code_page: CodePage, index: usize, bytes: []const u8) ?Codepoint {
236 if (index >= bytes.len) return null;
237 switch (code_page) {
238 .windows1252 => {
239 // All byte values have a representation, so just convert the byte
240 return Codepoint{
241 .value = windows1252.toCodepoint(bytes[index]),
242 .byte_len = 1,
243 };
244 },
245 .utf8 => {
246 return Utf8.WellFormedDecoder.decode(bytes[index..]);
247 },
248 else => unreachable,
249 }
250 }
251
252 pub fn isSupported(code_page: CodePage) bool {
253 return switch (code_page) {
254 .windows1252, .utf8 => true,
255 else => false,
256 };
257 }
258
259 pub fn getByIdentifier(identifier: u16) !CodePage {
260 // There's probably a more efficient way to do this (e.g. ComptimeHashMap?) but
261 // this should be fine, especially since this function likely won't be called much.
262 inline for (@typeInfo(CodePage).Enum.fields) |enumField| {
263 if (identifier == enumField.value) {
264 return @field(CodePage, enumField.name);
265 }
266 }
267 return error.InvalidCodePage;
268 }
269
270 pub fn getByIdentifierEnsureSupported(identifier: u16) !CodePage {
271 const code_page = try getByIdentifier(identifier);
272 switch (isSupported(code_page)) {
273 true => return code_page,
274 false => return error.UnsupportedCodePage,
275 }
276 }
277};
278
279pub const Utf8 = struct {
280 /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section
281 /// D92 of Chapter 3 of the Unicode standard (Table 3-7 specifically).
282 pub const WellFormedDecoder = struct {
283 /// Like std.unicode.utf8ByteSequenceLength, but:
284 /// - Rejects non-well-formed first bytes, i.e. C0-C1, F5-FF
285 /// - Returns an optional value instead of an error union
286 pub fn sequenceLength(first_byte: u8) ?u3 {
287 return switch (first_byte) {
288 0x00...0x7F => 1,
289 0xC2...0xDF => 2,
290 0xE0...0xEF => 3,
291 0xF0...0xF4 => 4,
292 else => null,
293 };
294 }
295
296 fn isContinuationByte(byte: u8) bool {
297 return switch (byte) {
298 0x80...0xBF => true,
299 else => false,
300 };
301 }
302
303 pub fn decode(bytes: []const u8) Codepoint {
304 std.debug.assert(bytes.len > 0);
305 const first_byte = bytes[0];
306 const expected_len = sequenceLength(first_byte) orelse {
307 return .{ .value = Codepoint.invalid, .byte_len = 1 };
308 };
309 if (expected_len == 1) return .{ .value = first_byte, .byte_len = 1 };
310
311 var value: u21 = first_byte & 0b00011111;
312 var byte_index: u8 = 1;
313 while (byte_index < @min(bytes.len, expected_len)) : (byte_index += 1) {
314 const byte = bytes[byte_index];
315 // See Table 3-7 of D92 in Chapter 3 of the Unicode Standard
316 const valid: bool = switch (byte_index) {
317 1 => switch (first_byte) {
318 0xE0 => switch (byte) {
319 0xA0...0xBF => true,
320 else => false,
321 },
322 0xED => switch (byte) {
323 0x80...0x9F => true,
324 else => false,
325 },
326 0xF0 => switch (byte) {
327 0x90...0xBF => true,
328 else => false,
329 },
330 0xF4 => switch (byte) {
331 0x80...0x8F => true,
332 else => false,
333 },
334 else => switch (byte) {
335 0x80...0xBF => true,
336 else => false,
337 },
338 },
339 else => switch (byte) {
340 0x80...0xBF => true,
341 else => false,
342 },
343 };
344
345 if (!valid) {
346 var len = byte_index;
347 // Only include the byte in the invalid sequence if it's in the range
348 // of a continuation byte. All other values should not be included in the
349 // invalid sequence.
350 //
351 // Note: This is how the Windows RC compiler handles this, this may not
352 // be the correct-as-according-to-the-Unicode-standard way to do it.
353 if (isContinuationByte(byte)) len += 1;
354 return .{ .value = Codepoint.invalid, .byte_len = len };
355 }
356
357 value <<= 6;
358 value |= byte & 0b00111111;
359 }
360 if (byte_index != expected_len) {
361 return .{ .value = Codepoint.invalid, .byte_len = byte_index };
362 }
363 return .{ .value = value, .byte_len = expected_len };
364 }
365 };
366};
367
368test "Utf8.WellFormedDecoder" {
369 const invalid_utf8 = "\xF0\x80";
370 const decoded = Utf8.WellFormedDecoder.decode(invalid_utf8);
371 try std.testing.expectEqual(Codepoint.invalid, decoded.value);
372 try std.testing.expectEqual(@as(usize, 2), decoded.byte_len);
373}
374
375test "codepointAt invalid utf8" {
376 {
377 const invalid_utf8 = "\xf0\xf0\x80\x80\x80";
378 try std.testing.expectEqual(Codepoint{
379 .value = Codepoint.invalid,
380 .byte_len = 1,
381 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
382 try std.testing.expectEqual(Codepoint{
383 .value = Codepoint.invalid,
384 .byte_len = 2,
385 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);
386 try std.testing.expectEqual(Codepoint{
387 .value = Codepoint.invalid,
388 .byte_len = 1,
389 }, CodePage.utf8.codepointAt(3, invalid_utf8).?);
390 try std.testing.expectEqual(Codepoint{
391 .value = Codepoint.invalid,
392 .byte_len = 1,
393 }, CodePage.utf8.codepointAt(4, invalid_utf8).?);
394 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(5, invalid_utf8));
395 }
396
397 {
398 const invalid_utf8 = "\xE1\xA0\xC0";
399 try std.testing.expectEqual(Codepoint{
400 .value = Codepoint.invalid,
401 .byte_len = 2,
402 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
403 try std.testing.expectEqual(Codepoint{
404 .value = Codepoint.invalid,
405 .byte_len = 1,
406 }, CodePage.utf8.codepointAt(2, invalid_utf8).?);
407 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(3, invalid_utf8));
408 }
409
410 {
411 const invalid_utf8 = "\xD2";
412 try std.testing.expectEqual(Codepoint{
413 .value = Codepoint.invalid,
414 .byte_len = 1,
415 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
416 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, invalid_utf8));
417 }
418
419 {
420 const invalid_utf8 = "\xE1\xA0";
421 try std.testing.expectEqual(Codepoint{
422 .value = Codepoint.invalid,
423 .byte_len = 2,
424 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
425 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));
426 }
427
428 {
429 const invalid_utf8 = "\xC5\xFF";
430 try std.testing.expectEqual(Codepoint{
431 .value = Codepoint.invalid,
432 .byte_len = 1,
433 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
434 try std.testing.expectEqual(Codepoint{
435 .value = Codepoint.invalid,
436 .byte_len = 1,
437 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);
438 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));
439 }
440}
441
442test "codepointAt utf8 encoded" {
443 const utf8_encoded = "²";
444
445 // with code page utf8
446 try std.testing.expectEqual(Codepoint{
447 .value = '²',
448 .byte_len = 2,
449 }, CodePage.utf8.codepointAt(0, utf8_encoded).?);
450 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, utf8_encoded));
451
452 // with code page windows1252
453 try std.testing.expectEqual(Codepoint{
454 .value = '\xC2',
455 .byte_len = 1,
456 }, CodePage.windows1252.codepointAt(0, utf8_encoded).?);
457 try std.testing.expectEqual(Codepoint{
458 .value = '\xB2',
459 .byte_len = 1,
460 }, CodePage.windows1252.codepointAt(1, utf8_encoded).?);
461 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, utf8_encoded));
462}
463
464test "codepointAt windows1252 encoded" {
465 const windows1252_encoded = "\xB2";
466
467 // with code page utf8
468 try std.testing.expectEqual(Codepoint{
469 .value = Codepoint.invalid,
470 .byte_len = 1,
471 }, CodePage.utf8.codepointAt(0, windows1252_encoded).?);
472 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, windows1252_encoded));
473
474 // with code page windows1252
475 try std.testing.expectEqual(Codepoint{
476 .value = '\xB2',
477 .byte_len = 1,
478 }, CodePage.windows1252.codepointAt(0, windows1252_encoded).?);
479 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, windows1252_encoded));
480}
481
482pub const Codepoint = struct {
483 value: u21,
484 byte_len: usize,
485
486 pub const invalid: u21 = std.math.maxInt(u21);
487};
src/resinator/comments.zig deleted-340
...@@ -1,340 +0,0 @@
1//! Expects to run after a C preprocessor step that preserves comments.
2//!
3//! `rc` has a peculiar quirk where something like `blah/**/blah` will be
4//! transformed into `blahblah` during parsing. However, `clang -E` will
5//! transform it into `blah blah`, so in order to match `rc`, we need
6//! to remove comments ourselves after the preprocessor runs.
7//! Note: Multiline comments that actually span more than one line do
8//! get translated to a space character by `rc`.
9//!
10//! Removing comments before lexing also allows the lexer to not have to
11//! deal with comments which would complicate its implementation (this is something
12//! of a tradeoff, as removing comments in a separate pass means that we'll
13//! need to iterate the source twice instead of once, but having to deal with
14//! comments when lexing would be a pain).
15
16const std = @import("std");
17const Allocator = std.mem.Allocator;
18const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter;
19const SourceMappings = @import("source_mapping.zig").SourceMappings;
20const LineHandler = @import("lex.zig").LineHandler;
21const formsLineEndingPair = @import("source_mapping.zig").formsLineEndingPair;
22
23/// `buf` must be at least as long as `source`
24/// In-place transformation is supported (i.e. `source` and `buf` can be the same slice)
25pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMappings) []u8 {
26 std.debug.assert(buf.len >= source.len);
27 var result = UncheckedSliceWriter{ .slice = buf };
28 const State = enum {
29 start,
30 forward_slash,
31 line_comment,
32 multiline_comment,
33 multiline_comment_end,
34 single_quoted,
35 single_quoted_escape,
36 double_quoted,
37 double_quoted_escape,
38 };
39 var state: State = .start;
40 var index: usize = 0;
41 var pending_start: ?usize = null;
42 var line_handler = LineHandler{ .buffer = source };
43 while (index < source.len) : (index += 1) {
44 const c = source[index];
45 // TODO: Disallow \x1A, \x00, \x7F in comments. At least \x1A and \x00 can definitely
46 // cause errors or parsing weirdness in the Win32 RC compiler. These are disallowed
47 // in the lexer, but comments are stripped before getting to the lexer.
48 switch (state) {
49 .start => switch (c) {
50 '/' => {
51 state = .forward_slash;
52 pending_start = index;
53 },
54 '\r', '\n' => {
55 _ = line_handler.incrementLineNumber(index);
56 result.write(c);
57 },
58 else => {
59 switch (c) {
60 '"' => state = .double_quoted,
61 '\'' => state = .single_quoted,
62 else => {},
63 }
64 result.write(c);
65 },
66 },
67 .forward_slash => switch (c) {
68 '/' => state = .line_comment,
69 '*' => {
70 state = .multiline_comment;
71 },
72 else => {
73 _ = line_handler.maybeIncrementLineNumber(index);
74 result.writeSlice(source[pending_start.? .. index + 1]);
75 pending_start = null;
76 state = .start;
77 },
78 },
79 .line_comment => switch (c) {
80 '\r', '\n' => {
81 _ = line_handler.incrementLineNumber(index);
82 result.write(c);
83 state = .start;
84 },
85 else => {},
86 },
87 .multiline_comment => switch (c) {
88 '\r' => handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings),
89 '\n' => {
90 _ = line_handler.incrementLineNumber(index);
91 result.write(c);
92 },
93 '*' => state = .multiline_comment_end,
94 else => {},
95 },
96 .multiline_comment_end => switch (c) {
97 '\r' => {
98 handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings);
99 // We only want to treat this as a newline if it's part of a CRLF pair. If it's
100 // not, then we still want to stay in .multiline_comment_end, so that e.g. `*<\r>/` still
101 // functions as a `*/` comment ending. Kinda crazy, but that's how the Win32 implementation works.
102 if (formsLineEndingPair(source, '\r', index + 1)) {
103 state = .multiline_comment;
104 }
105 },
106 '\n' => {
107 _ = line_handler.incrementLineNumber(index);
108 result.write(c);
109 state = .multiline_comment;
110 },
111 '/' => {
112 state = .start;
113 },
114 else => {
115 state = .multiline_comment;
116 },
117 },
118 .single_quoted => switch (c) {
119 '\r', '\n' => {
120 _ = line_handler.incrementLineNumber(index);
121 state = .start;
122 result.write(c);
123 },
124 '\\' => {
125 state = .single_quoted_escape;
126 result.write(c);
127 },
128 '\'' => {
129 state = .start;
130 result.write(c);
131 },
132 else => {
133 result.write(c);
134 },
135 },
136 .single_quoted_escape => switch (c) {
137 '\r', '\n' => {
138 _ = line_handler.incrementLineNumber(index);
139 state = .start;
140 result.write(c);
141 },
142 else => {
143 state = .single_quoted;
144 result.write(c);
145 },
146 },
147 .double_quoted => switch (c) {
148 '\r', '\n' => {
149 _ = line_handler.incrementLineNumber(index);
150 state = .start;
151 result.write(c);
152 },
153 '\\' => {
154 state = .double_quoted_escape;
155 result.write(c);
156 },
157 '"' => {
158 state = .start;
159 result.write(c);
160 },
161 else => {
162 result.write(c);
163 },
164 },
165 .double_quoted_escape => switch (c) {
166 '\r', '\n' => {
167 _ = line_handler.incrementLineNumber(index);
168 state = .start;
169 result.write(c);
170 },
171 else => {
172 state = .double_quoted;
173 result.write(c);
174 },
175 },
176 }
177 }
178 return result.getWritten();
179}
180
181inline fn handleMultilineCarriageReturn(
182 source: []const u8,
183 line_handler: *LineHandler,
184 index: usize,
185 result: *UncheckedSliceWriter,
186 source_mappings: ?*SourceMappings,
187) void {
188 // Note: Bare \r within a multiline comment should *not* be treated as a line ending for the
189 // purposes of removing comments, but *should* be treated as a line ending for the
190 // purposes of line counting/source mapping
191 _ = line_handler.incrementLineNumber(index);
192 // So only write the \r if it's part of a CRLF pair
193 if (formsLineEndingPair(source, '\r', index + 1)) {
194 result.write('\r');
195 }
196 // And otherwise, we want to collapse the source mapping so that we can still know which
197 // line came from where.
198 else {
199 // Because the line gets collapsed, we need to decrement line number so that
200 // the next collapse acts on the first of the collapsed line numbers
201 line_handler.line_number -= 1;
202 if (source_mappings) |mappings| {
203 mappings.collapse(line_handler.line_number, 1);
204 }
205 }
206}
207
208pub fn removeCommentsAlloc(allocator: Allocator, source: []const u8, source_mappings: ?*SourceMappings) ![]u8 {
209 const buf = try allocator.alloc(u8, source.len);
210 errdefer allocator.free(buf);
211 const result = removeComments(source, buf, source_mappings);
212 return allocator.realloc(buf, result.len);
213}
214
215fn testRemoveComments(expected: []const u8, source: []const u8) !void {
216 const result = try removeCommentsAlloc(std.testing.allocator, source, null);
217 defer std.testing.allocator.free(result);
218
219 try std.testing.expectEqualStrings(expected, result);
220}
221
222test "basic" {
223 try testRemoveComments("", "// comment");
224 try testRemoveComments("", "/* comment */");
225}
226
227test "mixed" {
228 try testRemoveComments("hello", "hello// comment");
229 try testRemoveComments("hello", "hel/* comment */lo");
230}
231
232test "within a string" {
233 // escaped " is \"
234 try testRemoveComments(
235 \\blah"//som\"/*ething*/"BLAH
236 ,
237 \\blah"//som\"/*ething*/"BLAH
238 );
239}
240
241test "line comments retain newlines" {
242 try testRemoveComments(
243 \\
244 \\
245 \\
246 ,
247 \\// comment
248 \\// comment
249 \\// comment
250 );
251
252 try testRemoveComments("\r\n", "//comment\r\n");
253}
254
255test "crazy" {
256 try testRemoveComments(
257 \\blah"/*som*/\""BLAH
258 ,
259 \\blah"/*som*/\""/*ething*/BLAH
260 );
261
262 try testRemoveComments(
263 \\blah"/*som*/"BLAH RCDATA "BEGIN END
264 \\
265 \\
266 \\hello
267 \\"
268 ,
269 \\blah"/*som*/"/*ething*/BLAH RCDATA "BEGIN END
270 \\// comment
271 \\//"blah blah" RCDATA {}
272 \\hello
273 \\"
274 );
275}
276
277test "multiline comment with newlines" {
278 // bare \r is not treated as a newline
279 try testRemoveComments("blahblah", "blah/*some\rthing*/blah");
280
281 try testRemoveComments(
282 \\blah
283 \\blah
284 ,
285 \\blah/*some
286 \\thing*/blah
287 );
288 try testRemoveComments(
289 "blah\r\nblah",
290 "blah/*some\r\nthing*/blah",
291 );
292
293 // handle *<not /> correctly
294 try testRemoveComments(
295 \\blah
296 \\
297 \\
298 ,
299 \\blah/*some
300 \\thing*
301 \\/bl*ah*/
302 );
303}
304
305test "comments appended to a line" {
306 try testRemoveComments(
307 \\blah
308 \\blah
309 ,
310 \\blah // line comment
311 \\blah
312 );
313 try testRemoveComments(
314 "blah \r\nblah",
315 "blah // line comment\r\nblah",
316 );
317}
318
319test "remove comments with mappings" {
320 const allocator = std.testing.allocator;
321 var mut_source = "blah/*\rcommented line*\r/blah".*;
322 var mappings = SourceMappings{};
323 _ = try mappings.files.put(allocator, "test.rc");
324 try mappings.set(allocator, 1, .{ .start_line = 1, .end_line = 1, .filename_offset = 0 });
325 try mappings.set(allocator, 2, .{ .start_line = 2, .end_line = 2, .filename_offset = 0 });
326 try mappings.set(allocator, 3, .{ .start_line = 3, .end_line = 3, .filename_offset = 0 });
327 defer mappings.deinit(allocator);
328
329 const result = removeComments(&mut_source, &mut_source, &mappings);
330
331 try std.testing.expectEqualStrings("blahblah", result);
332 try std.testing.expectEqual(@as(usize, 1), mappings.mapping.items.len);
333 try std.testing.expectEqual(@as(usize, 3), mappings.mapping.items[0].end_line);
334}
335
336test "in place" {
337 var mut_source = "blah /* comment */ blah".*;
338 const result = removeComments(&mut_source, &mut_source, null);
339 try std.testing.expectEqualStrings("blah blah", result);
340}
src/resinator/compile.zig deleted-3378
...@@ -1,3378 +0,0 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const Node = @import("ast.zig").Node;
5const lex = @import("lex.zig");
6const Parser = @import("parse.zig").Parser;
7const Resource = @import("rc.zig").Resource;
8const Token = @import("lex.zig").Token;
9const literals = @import("literals.zig");
10const Number = literals.Number;
11const SourceBytes = literals.SourceBytes;
12const Diagnostics = @import("errors.zig").Diagnostics;
13const ErrorDetails = @import("errors.zig").ErrorDetails;
14const MemoryFlags = @import("res.zig").MemoryFlags;
15const rc = @import("rc.zig");
16const res = @import("res.zig");
17const ico = @import("ico.zig");
18const ani = @import("ani.zig");
19const bmp = @import("bmp.zig");
20const WORD = std.os.windows.WORD;
21const DWORD = std.os.windows.DWORD;
22const utils = @import("utils.zig");
23const NameOrOrdinal = res.NameOrOrdinal;
24const CodePage = @import("code_pages.zig").CodePage;
25const CodePageLookup = @import("ast.zig").CodePageLookup;
26const SourceMappings = @import("source_mapping.zig").SourceMappings;
27const windows1252 = @import("windows1252.zig");
28const lang = @import("lang.zig");
29const code_pages = @import("code_pages.zig");
30const errors = @import("errors.zig");
31const native_endian = builtin.cpu.arch.endian();
32
33pub const CompileOptions = struct {
34 cwd: std.fs.Dir,
35 diagnostics: *Diagnostics,
36 source_mappings: ?*SourceMappings = null,
37 /// List of paths (absolute or relative to `cwd`) for every file that the resources within the .rc file depend on.
38 /// Items within the list will be allocated using the allocator of the ArrayList and must be
39 /// freed by the caller.
40 /// TODO: Maybe a dedicated struct for this purpose so that it's a bit nicer to work with.
41 dependencies_list: ?*std.ArrayList([]const u8) = null,
42 default_code_page: CodePage = .windows1252,
43 ignore_include_env_var: bool = false,
44 extra_include_paths: []const []const u8 = &.{},
45 /// This is just an API convenience to allow separately passing 'system' (i.e. those
46 /// that would normally be gotten from the INCLUDE env var) include paths. This is mostly
47 /// intended for use when setting `ignore_include_env_var = true`. When `ignore_include_env_var`
48 /// is false, `system_include_paths` will be searched before the paths in the INCLUDE env var.
49 system_include_paths: []const []const u8 = &.{},
50 default_language_id: ?u16 = null,
51 // TODO: Implement verbose output
52 verbose: bool = false,
53 null_terminate_string_table_strings: bool = false,
54 /// Note: This is a u15 to ensure that the maximum number of UTF-16 code units
55 /// plus a null-terminator can always fit into a u16.
56 max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
57 silent_duplicate_control_ids: bool = false,
58 warn_instead_of_error_on_invalid_code_page: bool = false,
59};
60
61pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, options: CompileOptions) !void {
62 var lexer = lex.Lexer.init(source, .{
63 .default_code_page = options.default_code_page,
64 .source_mappings = options.source_mappings,
65 .max_string_literal_codepoints = options.max_string_literal_codepoints,
66 });
67 var parser = Parser.init(&lexer, .{
68 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
69 });
70 var tree = try parser.parse(allocator, options.diagnostics);
71 defer tree.deinit();
72
73 var search_dirs = std.ArrayList(SearchDir).init(allocator);
74 defer {
75 for (search_dirs.items) |*search_dir| {
76 search_dir.deinit(allocator);
77 }
78 search_dirs.deinit();
79 }
80
81 if (options.source_mappings) |source_mappings| {
82 const root_path = source_mappings.files.get(source_mappings.root_filename_offset);
83 // If dirname returns null, then the root path will be the same as
84 // the cwd so we don't need to add it as a distinct search path.
85 if (std.fs.path.dirname(root_path)) |root_dir_path| {
86 var root_dir = try options.cwd.openDir(root_dir_path, .{});
87 errdefer root_dir.close();
88 try search_dirs.append(.{ .dir = root_dir, .path = try allocator.dupe(u8, root_dir_path) });
89 }
90 }
91 // Re-open the passed in cwd since we want to be able to close it (std.fs.cwd() shouldn't be closed)
92 const cwd_dir = options.cwd.openDir(".", .{}) catch |err| {
93 try options.diagnostics.append(.{
94 .err = .failed_to_open_cwd,
95 .token = .{
96 .id = .invalid,
97 .start = 0,
98 .end = 0,
99 .line_number = 1,
100 },
101 .print_source_line = false,
102 .extra = .{ .file_open_error = .{
103 .err = ErrorDetails.FileOpenError.enumFromError(err),
104 .filename_string_index = undefined,
105 } },
106 });
107 return error.CompileError;
108 };
109 try search_dirs.append(.{ .dir = cwd_dir, .path = null });
110 for (options.extra_include_paths) |extra_include_path| {
111 var dir = openSearchPathDir(options.cwd, extra_include_path) catch {
112 // TODO: maybe a warning that the search path is skipped?
113 continue;
114 };
115 errdefer dir.close();
116 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, extra_include_path) });
117 }
118 for (options.system_include_paths) |system_include_path| {
119 var dir = openSearchPathDir(options.cwd, system_include_path) catch {
120 // TODO: maybe a warning that the search path is skipped?
121 continue;
122 };
123 errdefer dir.close();
124 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, system_include_path) });
125 }
126 if (!options.ignore_include_env_var) {
127 const INCLUDE = std.process.getEnvVarOwned(allocator, "INCLUDE") catch "";
128 defer allocator.free(INCLUDE);
129
130 // The only precedence here is llvm-rc which also uses the platform-specific
131 // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
132 const delimiter = switch (builtin.os.tag) {
133 .windows => ';',
134 else => ':',
135 };
136 var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
137 while (it.next()) |search_path| {
138 var dir = openSearchPathDir(options.cwd, search_path) catch continue;
139 errdefer dir.close();
140 try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, search_path) });
141 }
142 }
143
144 var arena_allocator = std.heap.ArenaAllocator.init(allocator);
145 defer arena_allocator.deinit();
146 const arena = arena_allocator.allocator();
147
148 var compiler = Compiler{
149 .source = source,
150 .arena = arena,
151 .allocator = allocator,
152 .cwd = options.cwd,
153 .diagnostics = options.diagnostics,
154 .dependencies_list = options.dependencies_list,
155 .input_code_pages = &tree.input_code_pages,
156 .output_code_pages = &tree.output_code_pages,
157 // This is only safe because we know search_dirs won't be modified past this point
158 .search_dirs = search_dirs.items,
159 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
160 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
161 };
162 if (options.default_language_id) |default_language_id| {
163 compiler.state.language = res.Language.fromInt(default_language_id);
164 }
165
166 try compiler.writeRoot(tree.root(), writer);
167}
168
169pub const Compiler = struct {
170 source: []const u8,
171 arena: Allocator,
172 allocator: Allocator,
173 cwd: std.fs.Dir,
174 state: State = .{},
175 diagnostics: *Diagnostics,
176 dependencies_list: ?*std.ArrayList([]const u8),
177 input_code_pages: *const CodePageLookup,
178 output_code_pages: *const CodePageLookup,
179 search_dirs: []SearchDir,
180 null_terminate_string_table_strings: bool,
181 silent_duplicate_control_ids: bool,
182
183 pub const State = struct {
184 icon_id: u16 = 1,
185 string_tables: StringTablesByLanguage = .{},
186 language: res.Language = .{},
187 font_dir: FontDir = .{},
188 version: u32 = 0,
189 characteristics: u32 = 0,
190 };
191
192 pub fn writeRoot(self: *Compiler, root: *Node.Root, writer: anytype) !void {
193 try writeEmptyResource(writer);
194 for (root.body) |node| {
195 try self.writeNode(node, writer);
196 }
197
198 // now write the FONTDIR (if it has anything in it)
199 try self.state.font_dir.writeResData(self, writer);
200 if (self.state.font_dir.fonts.items.len != 0) {
201 // The Win32 RC compiler may write a different FONTDIR resource than us,
202 // due to it sometimes writing a non-zero-length device name/face name
203 // whereas we *always* write them both as zero-length.
204 //
205 // In practical terms, this doesn't matter, since for various reasons the format
206 // of the FONTDIR cannot be relied on and is seemingly not actually used by anything
207 // anymore. We still want to emit some sort of diagnostic for the purposes of being able
208 // to know that our .RES is intentionally not meant to be byte-for-byte identical with
209 // the rc.exe output.
210 //
211 // By using the hint type here, we allow this diagnostic to be detected in code,
212 // but it will not be printed since the end-user doesn't need to care.
213 try self.addErrorDetails(.{
214 .err = .result_contains_fontdir,
215 .type = .hint,
216 .token = undefined,
217 });
218 }
219 // once we've written every else out, we can write out the finalized STRINGTABLE resources
220 var string_tables_it = self.state.string_tables.tables.iterator();
221 while (string_tables_it.next()) |string_table_entry| {
222 var string_table_it = string_table_entry.value_ptr.blocks.iterator();
223 while (string_table_it.next()) |entry| {
224 try entry.value_ptr.writeResData(self, string_table_entry.key_ptr.*, entry.key_ptr.*, writer);
225 }
226 }
227 }
228
229 pub fn writeNode(self: *Compiler, node: *Node, writer: anytype) !void {
230 switch (node.id) {
231 .root => unreachable, // writeRoot should be called directly instead
232 .resource_external => try self.writeResourceExternal(@fieldParentPtr(Node.ResourceExternal, "base", node), writer),
233 .resource_raw_data => try self.writeResourceRawData(@fieldParentPtr(Node.ResourceRawData, "base", node), writer),
234 .literal => unreachable, // this is context dependent and should be handled by its parent
235 .binary_expression => unreachable,
236 .grouped_expression => unreachable,
237 .not_expression => unreachable,
238 .invalid => {}, // no-op, currently only used for dangling literals at EOF
239 .accelerators => try self.writeAccelerators(@fieldParentPtr(Node.Accelerators, "base", node), writer),
240 .accelerator => unreachable, // handled by writeAccelerators
241 .dialog => try self.writeDialog(@fieldParentPtr(Node.Dialog, "base", node), writer),
242 .control_statement => unreachable,
243 .toolbar => try self.writeToolbar(@fieldParentPtr(Node.Toolbar, "base", node), writer),
244 .menu => try self.writeMenu(@fieldParentPtr(Node.Menu, "base", node), writer),
245 .menu_item => unreachable,
246 .menu_item_separator => unreachable,
247 .menu_item_ex => unreachable,
248 .popup => unreachable,
249 .popup_ex => unreachable,
250 .version_info => try self.writeVersionInfo(@fieldParentPtr(Node.VersionInfo, "base", node), writer),
251 .version_statement => unreachable,
252 .block => unreachable,
253 .block_value => unreachable,
254 .block_value_value => unreachable,
255 .string_table => try self.writeStringTable(@fieldParentPtr(Node.StringTable, "base", node)),
256 .string_table_string => unreachable, // handled by writeStringTable
257 .language_statement => self.writeLanguageStatement(@fieldParentPtr(Node.LanguageStatement, "base", node)),
258 .font_statement => unreachable,
259 .simple_statement => self.writeTopLevelSimpleStatement(@fieldParentPtr(Node.SimpleStatement, "base", node)),
260 }
261 }
262
263 /// Returns the filename encoded as UTF-8 (allocated by self.allocator)
264 pub fn evaluateFilenameExpression(self: *Compiler, expression_node: *Node) ![]u8 {
265 switch (expression_node.id) {
266 .literal => {
267 const literal_node = expression_node.cast(.literal).?;
268 switch (literal_node.token.id) {
269 .literal, .number => {
270 const slice = literal_node.token.slice(self.source);
271 const code_page = self.input_code_pages.getForToken(literal_node.token);
272 var buf = try std.ArrayList(u8).initCapacity(self.allocator, slice.len);
273 errdefer buf.deinit();
274
275 var index: usize = 0;
276 while (code_page.codepointAt(index, slice)) |codepoint| : (index += codepoint.byte_len) {
277 const c = codepoint.value;
278 if (c == code_pages.Codepoint.invalid) {
279 try buf.appendSlice("�");
280 } else {
281 // Anything that is not returned as an invalid codepoint must be encodable as UTF-8.
282 const utf8_len = std.unicode.utf8CodepointSequenceLength(c) catch unreachable;
283 try buf.ensureUnusedCapacity(utf8_len);
284 _ = std.unicode.utf8Encode(c, buf.unusedCapacitySlice()) catch unreachable;
285 buf.items.len += utf8_len;
286 }
287 }
288
289 return buf.toOwnedSlice();
290 },
291 .quoted_ascii_string, .quoted_wide_string => {
292 const slice = literal_node.token.slice(self.source);
293 const column = literal_node.token.calculateColumn(self.source, 8, null);
294 const bytes = SourceBytes{ .slice = slice, .code_page = self.input_code_pages.getForToken(literal_node.token) };
295
296 var buf = std.ArrayList(u8).init(self.allocator);
297 errdefer buf.deinit();
298
299 // Filenames are sort-of parsed as if they were wide strings, but the max escape width of
300 // hex/octal escapes is still determined by the L prefix. Since we want to end up with
301 // UTF-8, we can parse either string type directly to UTF-8.
302 var parser = literals.IterativeStringParser.init(bytes, .{
303 .start_column = column,
304 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
305 });
306
307 while (try parser.nextUnchecked()) |parsed| {
308 const c = parsed.codepoint;
309 if (c == code_pages.Codepoint.invalid) {
310 try buf.appendSlice("�");
311 } else {
312 var codepoint_buf: [4]u8 = undefined;
313 // If the codepoint cannot be encoded, we fall back to �
314 if (std.unicode.utf8Encode(c, &codepoint_buf)) |len| {
315 try buf.appendSlice(codepoint_buf[0..len]);
316 } else |_| {
317 try buf.appendSlice("�");
318 }
319 }
320 }
321
322 return buf.toOwnedSlice();
323 },
324 else => {
325 std.debug.print("unexpected filename token type: {}\n", .{literal_node.token});
326 unreachable; // no other token types should be in a filename literal node
327 },
328 }
329 },
330 .binary_expression => {
331 const binary_expression_node = expression_node.cast(.binary_expression).?;
332 return self.evaluateFilenameExpression(binary_expression_node.right);
333 },
334 .grouped_expression => {
335 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
336 return self.evaluateFilenameExpression(grouped_expression_node.expression);
337 },
338 else => unreachable,
339 }
340 }
341
342 /// https://learn.microsoft.com/en-us/windows/win32/menurc/searching-for-files
343 ///
344 /// Searches, in this order:
345 /// Directory of the 'root' .rc file (if different from CWD)
346 /// CWD
347 /// extra_include_paths (resolved relative to CWD)
348 /// system_include_paths (resolve relative to CWD)
349 /// INCLUDE environment var paths (only if ignore_include_env_var is false; resolved relative to CWD)
350 ///
351 /// Note: The CWD being searched *in addition to* the directory of the 'root' .rc file
352 /// is also how the Win32 RC compiler preprocessor searches for includes, but that
353 /// differs from how the clang preprocessor searches for includes.
354 ///
355 /// Note: This will always return the first matching file that can be opened.
356 /// This matches the Win32 RC compiler, which will fail with an error if the first
357 /// matching file is invalid. That is, it does not do the `cmd` PATH searching
358 /// thing of continuing to look for matching files until it finds a valid
359 /// one if a matching file is invalid.
360 fn searchForFile(self: *Compiler, path: []const u8) !std.fs.File {
361 // If the path is absolute, then it is not resolved relative to any search
362 // paths, so there's no point in checking them.
363 //
364 // This behavior was determined/confirmed with the following test:
365 // - A `test.rc` file with the contents `1 RCDATA "/test.bin"`
366 // - A `test.bin` file at `C:\test.bin`
367 // - A `test.bin` file at `inc\test.bin` relative to the .rc file
368 // - Invoking `rc` with `rc /i inc test.rc`
369 //
370 // This results in a .res file with the contents of `C:\test.bin`, not
371 // the contents of `inc\test.bin`. Further, if `C:\test.bin` is deleted,
372 // then it start failing to find `/test.bin`, meaning that it does not resolve
373 // `/test.bin` relative to include paths and instead only treats it as
374 // an absolute path.
375 if (std.fs.path.isAbsolute(path)) {
376 const file = try utils.openFileNotDir(std.fs.cwd(), path, .{});
377 errdefer file.close();
378
379 if (self.dependencies_list) |dependencies_list| {
380 const duped_path = try dependencies_list.allocator.dupe(u8, path);
381 errdefer dependencies_list.allocator.free(duped_path);
382 try dependencies_list.append(duped_path);
383 }
384 }
385
386 var first_error: ?std.fs.File.OpenError = null;
387 for (self.search_dirs) |search_dir| {
388 if (utils.openFileNotDir(search_dir.dir, path, .{})) |file| {
389 errdefer file.close();
390
391 if (self.dependencies_list) |dependencies_list| {
392 const searched_file_path = try std.fs.path.join(dependencies_list.allocator, &.{
393 search_dir.path orelse "", path,
394 });
395 errdefer dependencies_list.allocator.free(searched_file_path);
396 try dependencies_list.append(searched_file_path);
397 }
398
399 return file;
400 } else |err| if (first_error == null) {
401 first_error = err;
402 }
403 }
404 return first_error orelse error.FileNotFound;
405 }
406
407 pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: anytype) !void {
408 // Init header with data size zero for now, will need to fill it in later
409 var header = try self.resourceHeader(node.id, node.type, .{});
410 defer header.deinit(self.allocator);
411
412 const maybe_predefined_type = header.predefinedResourceType();
413
414 // DLGINCLUDE has special handling that doesn't actually need the file to exist
415 if (maybe_predefined_type != null and maybe_predefined_type.? == .DLGINCLUDE) {
416 const filename_token = node.filename.cast(.literal).?.token;
417 const parsed_filename = try self.parseQuotedStringAsAsciiString(filename_token);
418 defer self.allocator.free(parsed_filename);
419
420 header.applyMemoryFlags(node.common_resource_attributes, self.source);
421 header.data_size = @intCast(parsed_filename.len + 1);
422 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
423 try writer.writeAll(parsed_filename);
424 try writer.writeByte(0);
425 try writeDataPadding(writer, header.data_size);
426 return;
427 }
428
429 const filename_utf8 = try self.evaluateFilenameExpression(node.filename);
430 defer self.allocator.free(filename_utf8);
431
432 // TODO: More robust checking of the validity of the filename.
433 // This currently only checks for NUL bytes, but it should probably also check for
434 // platform-specific invalid characters like '*', '?', '"', '<', '>', '|' (Windows)
435 // Related: https://github.com/ziglang/zig/pull/14533#issuecomment-1416888193
436 if (std.mem.indexOfScalar(u8, filename_utf8, 0) != null) {
437 return self.addErrorDetailsAndFail(.{
438 .err = .invalid_filename,
439 .token = node.filename.getFirstToken(),
440 .token_span_end = node.filename.getLastToken(),
441 .extra = .{ .number = 0 },
442 });
443 }
444
445 // Allow plain number literals, but complex number expressions are evaluated strangely
446 // and almost certainly lead to things not intended by the user (e.g. '(1+-1)' evaluates
447 // to the filename '-1'), so error if the filename node is a grouped/binary expression.
448 // Note: This is done here instead of during parsing so that we can easily include
449 // the evaluated filename as part of the error messages.
450 if (node.filename.id != .literal) {
451 const filename_string_index = try self.diagnostics.putString(filename_utf8);
452 try self.addErrorDetails(.{
453 .err = .number_expression_as_filename,
454 .token = node.filename.getFirstToken(),
455 .token_span_end = node.filename.getLastToken(),
456 .extra = .{ .number = filename_string_index },
457 });
458 return self.addErrorDetailsAndFail(.{
459 .err = .number_expression_as_filename,
460 .type = .note,
461 .token = node.filename.getFirstToken(),
462 .token_span_end = node.filename.getLastToken(),
463 .print_source_line = false,
464 .extra = .{ .number = filename_string_index },
465 });
466 }
467 // From here on out, we know that the filename must be comprised of a single token,
468 // so get it here to simplify future usage.
469 const filename_token = node.filename.getFirstToken();
470
471 const file = self.searchForFile(filename_utf8) catch |err| switch (err) {
472 error.OutOfMemory => |e| return e,
473 else => |e| {
474 const filename_string_index = try self.diagnostics.putString(filename_utf8);
475 return self.addErrorDetailsAndFail(.{
476 .err = .file_open_error,
477 .token = filename_token,
478 .extra = .{ .file_open_error = .{
479 .err = ErrorDetails.FileOpenError.enumFromError(e),
480 .filename_string_index = filename_string_index,
481 } },
482 });
483 },
484 };
485 defer file.close();
486
487 if (maybe_predefined_type) |predefined_type| {
488 switch (predefined_type) {
489 .GROUP_ICON, .GROUP_CURSOR => {
490 // Check for animated icon first
491 if (ani.isAnimatedIcon(file.reader())) {
492 // Animated icons are just put into the resource unmodified,
493 // and the resource type changes to ANIICON/ANICURSOR
494
495 const new_predefined_type: res.RT = switch (predefined_type) {
496 .GROUP_ICON => .ANIICON,
497 .GROUP_CURSOR => .ANICURSOR,
498 else => unreachable,
499 };
500 header.type_value.ordinal = @intFromEnum(new_predefined_type);
501 header.memory_flags = MemoryFlags.defaults(new_predefined_type);
502 header.applyMemoryFlags(node.common_resource_attributes, self.source);
503 header.data_size = @intCast(try file.getEndPos());
504
505 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
506 try file.seekTo(0);
507 try writeResourceData(writer, file.reader(), header.data_size);
508 return;
509 }
510
511 // isAnimatedIcon moved the file cursor so reset to the start
512 try file.seekTo(0);
513
514 const icon_dir = ico.read(self.allocator, file.reader(), try file.getEndPos()) catch |err| switch (err) {
515 error.OutOfMemory => |e| return e,
516 else => |e| {
517 return self.iconReadError(
518 e,
519 filename_utf8,
520 filename_token,
521 predefined_type,
522 );
523 },
524 };
525 defer icon_dir.deinit();
526
527 // This limit is inherent to the ico format since number of entries is a u16 field.
528 std.debug.assert(icon_dir.entries.len <= std.math.maxInt(u16));
529
530 // Note: The Win32 RC compiler will compile the resource as whatever type is
531 // in the icon_dir regardless of the type of resource specified in the .rc.
532 // This leads to unusable .res files when the types mismatch, so
533 // we error instead.
534 const res_types_match = switch (predefined_type) {
535 .GROUP_ICON => icon_dir.image_type == .icon,
536 .GROUP_CURSOR => icon_dir.image_type == .cursor,
537 else => unreachable,
538 };
539 if (!res_types_match) {
540 return self.addErrorDetailsAndFail(.{
541 .err = .icon_dir_and_resource_type_mismatch,
542 .token = filename_token,
543 .extra = .{ .resource = switch (predefined_type) {
544 .GROUP_ICON => .icon,
545 .GROUP_CURSOR => .cursor,
546 else => unreachable,
547 } },
548 });
549 }
550
551 // Memory flags affect the RT_ICON and the RT_GROUP_ICON differently
552 var icon_memory_flags = MemoryFlags.defaults(res.RT.ICON);
553 applyToMemoryFlags(&icon_memory_flags, node.common_resource_attributes, self.source);
554 applyToGroupMemoryFlags(&header.memory_flags, node.common_resource_attributes, self.source);
555
556 const first_icon_id = self.state.icon_id;
557 const entry_type = if (predefined_type == .GROUP_ICON) @intFromEnum(res.RT.ICON) else @intFromEnum(res.RT.CURSOR);
558 for (icon_dir.entries, 0..) |*entry, entry_i_usize| {
559 // We know that the entry index must fit within a u16, so
560 // cast it here to simplify usage sites.
561 const entry_i: u16 = @intCast(entry_i_usize);
562 var full_data_size = entry.data_size_in_bytes;
563 if (icon_dir.image_type == .cursor) {
564 full_data_size = std.math.add(u32, full_data_size, 4) catch {
565 return self.addErrorDetailsAndFail(.{
566 .err = .resource_data_size_exceeds_max,
567 .token = node.id,
568 });
569 };
570 }
571
572 const image_header = ResourceHeader{
573 .type_value = .{ .ordinal = entry_type },
574 .name_value = .{ .ordinal = self.state.icon_id },
575 .data_size = full_data_size,
576 .memory_flags = icon_memory_flags,
577 .language = self.state.language,
578 .version = self.state.version,
579 .characteristics = self.state.characteristics,
580 };
581 try image_header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
582
583 // From https://learn.microsoft.com/en-us/windows/win32/menurc/localheader:
584 // > The LOCALHEADER structure is the first data written to the RT_CURSOR
585 // > resource if a RESDIR structure contains information about a cursor.
586 // where LOCALHEADER is `struct { WORD xHotSpot; WORD yHotSpot; }`
587 if (icon_dir.image_type == .cursor) {
588 try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_x, .little);
589 try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_y, .little);
590 }
591
592 try file.seekTo(entry.data_offset_from_start_of_file);
593 var header_bytes = file.reader().readBytesNoEof(16) catch {
594 return self.iconReadError(
595 error.UnexpectedEOF,
596 filename_utf8,
597 filename_token,
598 predefined_type,
599 );
600 };
601
602 const image_format = ico.ImageFormat.detect(&header_bytes);
603 if (!image_format.validate(&header_bytes)) {
604 return self.iconReadError(
605 error.InvalidHeader,
606 filename_utf8,
607 filename_token,
608 predefined_type,
609 );
610 }
611 switch (image_format) {
612 .riff => switch (icon_dir.image_type) {
613 .icon => {
614 // The Win32 RC compiler treats this as an error, but icon dirs
615 // with RIFF encoded icons within them work ~okay (they work
616 // in some places but not others, they may not animate, etc) if they are
617 // allowed to be compiled.
618 try self.addErrorDetails(.{
619 .err = .rc_would_error_on_icon_dir,
620 .type = .warning,
621 .token = filename_token,
622 .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
623 });
624 try self.addErrorDetails(.{
625 .err = .rc_would_error_on_icon_dir,
626 .type = .note,
627 .print_source_line = false,
628 .token = filename_token,
629 .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
630 });
631 },
632 .cursor => {
633 // The Win32 RC compiler errors in this case too, but we only error
634 // here because the cursor would fail to be loaded at runtime if we
635 // compiled it.
636 return self.addErrorDetailsAndFail(.{
637 .err = .format_not_supported_in_icon_dir,
638 .token = filename_token,
639 .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .riff, .index = entry_i } },
640 });
641 },
642 },
643 .png => switch (icon_dir.image_type) {
644 .icon => {
645 // PNG always seems to have 1 for color planes no matter what
646 entry.type_specific_data.icon.color_planes = 1;
647 // These seem to be the only values of num_colors that
648 // get treated specially
649 entry.type_specific_data.icon.bits_per_pixel = switch (entry.num_colors) {
650 2 => 1,
651 8 => 3,
652 16 => 4,
653 else => entry.type_specific_data.icon.bits_per_pixel,
654 };
655 },
656 .cursor => {
657 // The Win32 RC compiler treats this as an error, but cursor dirs
658 // with PNG encoded icons within them work fine if they are
659 // allowed to be compiled.
660 try self.addErrorDetails(.{
661 .err = .rc_would_error_on_icon_dir,
662 .type = .warning,
663 .token = filename_token,
664 .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .png, .index = entry_i } },
665 });
666 },
667 },
668 .dib => {
669 const bitmap_header: *ico.BitmapHeader = @ptrCast(@alignCast(&header_bytes));
670 if (native_endian == .big) {
671 std.mem.byteSwapAllFields(ico.BitmapHeader, bitmap_header);
672 }
673 const bitmap_version = ico.BitmapHeader.Version.get(bitmap_header.bcSize);
674
675 // The Win32 RC compiler only allows headers with
676 // `bcSize == sizeof(BITMAPINFOHEADER)`, but it seems unlikely
677 // that there's a good reason for that outside of too-old
678 // bitmap headers.
679 // TODO: Need to test V4 and V5 bitmaps to check they actually work
680 if (bitmap_version == .@"win2.0") {
681 return self.addErrorDetailsAndFail(.{
682 .err = .rc_would_error_on_bitmap_version,
683 .token = filename_token,
684 .extra = .{ .icon_dir = .{
685 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
686 .icon_format = image_format,
687 .index = entry_i,
688 .bitmap_version = bitmap_version,
689 } },
690 });
691 } else if (bitmap_version != .@"nt3.1") {
692 try self.addErrorDetails(.{
693 .err = .rc_would_error_on_bitmap_version,
694 .type = .warning,
695 .token = filename_token,
696 .extra = .{ .icon_dir = .{
697 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
698 .icon_format = image_format,
699 .index = entry_i,
700 .bitmap_version = bitmap_version,
701 } },
702 });
703 }
704
705 switch (icon_dir.image_type) {
706 .icon => {
707 // The values in the icon's BITMAPINFOHEADER always take precedence over
708 // the values in the IconDir, but not in the LOCALHEADER (see above).
709 entry.type_specific_data.icon.color_planes = bitmap_header.bcPlanes;
710 entry.type_specific_data.icon.bits_per_pixel = bitmap_header.bcBitCount;
711 },
712 .cursor => {
713 // Only cursors get the width/height from BITMAPINFOHEADER (icons don't)
714 entry.width = @intCast(bitmap_header.bcWidth);
715 entry.height = @intCast(bitmap_header.bcHeight);
716 entry.type_specific_data.cursor.hotspot_x = bitmap_header.bcPlanes;
717 entry.type_specific_data.cursor.hotspot_y = bitmap_header.bcBitCount;
718 },
719 }
720 },
721 }
722
723 try file.seekTo(entry.data_offset_from_start_of_file);
724 try writeResourceDataNoPadding(writer, file.reader(), entry.data_size_in_bytes);
725 try writeDataPadding(writer, full_data_size);
726
727 if (self.state.icon_id == std.math.maxInt(u16)) {
728 try self.addErrorDetails(.{
729 .err = .max_icon_ids_exhausted,
730 .print_source_line = false,
731 .token = filename_token,
732 .extra = .{ .icon_dir = .{
733 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
734 .icon_format = image_format,
735 .index = entry_i,
736 } },
737 });
738 return self.addErrorDetailsAndFail(.{
739 .err = .max_icon_ids_exhausted,
740 .type = .note,
741 .token = filename_token,
742 .extra = .{ .icon_dir = .{
743 .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
744 .icon_format = image_format,
745 .index = entry_i,
746 } },
747 });
748 }
749 self.state.icon_id += 1;
750 }
751
752 header.data_size = icon_dir.getResDataSize();
753
754 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
755 try icon_dir.writeResData(writer, first_icon_id);
756 try writeDataPadding(writer, header.data_size);
757 return;
758 },
759 .RCDATA, .HTML, .MANIFEST, .MESSAGETABLE, .DLGINIT, .PLUGPLAY => {
760 header.applyMemoryFlags(node.common_resource_attributes, self.source);
761 },
762 .BITMAP => {
763 header.applyMemoryFlags(node.common_resource_attributes, self.source);
764 const file_size = try file.getEndPos();
765
766 const bitmap_info = bmp.read(file.reader(), file_size) catch |err| {
767 const filename_string_index = try self.diagnostics.putString(filename_utf8);
768 return self.addErrorDetailsAndFail(.{
769 .err = .bmp_read_error,
770 .token = filename_token,
771 .extra = .{ .bmp_read_error = .{
772 .err = ErrorDetails.BitmapReadError.enumFromError(err),
773 .filename_string_index = filename_string_index,
774 } },
775 });
776 };
777
778 if (bitmap_info.getActualPaletteByteLen() > bitmap_info.getExpectedPaletteByteLen()) {
779 const num_ignored_bytes = bitmap_info.getActualPaletteByteLen() - bitmap_info.getExpectedPaletteByteLen();
780 var number_as_bytes: [8]u8 = undefined;
781 std.mem.writeInt(u64, &number_as_bytes, num_ignored_bytes, native_endian);
782 const value_string_index = try self.diagnostics.putString(&number_as_bytes);
783 try self.addErrorDetails(.{
784 .err = .bmp_ignored_palette_bytes,
785 .type = .warning,
786 .token = filename_token,
787 .extra = .{ .number = value_string_index },
788 });
789 } else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {
790 const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();
791
792 // TODO: Make this configurable (command line option)
793 const max_missing_bytes = 4096;
794 if (num_padding_bytes > max_missing_bytes) {
795 var numbers_as_bytes: [16]u8 = undefined;
796 std.mem.writeInt(u64, numbers_as_bytes[0..8], num_padding_bytes, native_endian);
797 std.mem.writeInt(u64, numbers_as_bytes[8..16], max_missing_bytes, native_endian);
798 const values_string_index = try self.diagnostics.putString(&numbers_as_bytes);
799 try self.addErrorDetails(.{
800 .err = .bmp_too_many_missing_palette_bytes,
801 .token = filename_token,
802 .extra = .{ .number = values_string_index },
803 });
804 return self.addErrorDetailsAndFail(.{
805 .err = .bmp_too_many_missing_palette_bytes,
806 .type = .note,
807 .print_source_line = false,
808 .token = filename_token,
809 });
810 }
811
812 var number_as_bytes: [8]u8 = undefined;
813 std.mem.writeInt(u64, &number_as_bytes, num_padding_bytes, native_endian);
814 const value_string_index = try self.diagnostics.putString(&number_as_bytes);
815 try self.addErrorDetails(.{
816 .err = .bmp_missing_palette_bytes,
817 .type = .warning,
818 .token = filename_token,
819 .extra = .{ .number = value_string_index },
820 });
821 const pixel_data_len = bitmap_info.getPixelDataLen(file_size);
822 if (pixel_data_len > 0) {
823 const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);
824 std.mem.writeInt(u64, &number_as_bytes, miscompiled_bytes, native_endian);
825 const miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);
826 try self.addErrorDetails(.{
827 .err = .rc_would_miscompile_bmp_palette_padding,
828 .type = .warning,
829 .token = filename_token,
830 .extra = .{ .number = miscompiled_bytes_string_index },
831 });
832 }
833 }
834
835 // TODO: It might be possible that the calculation done in this function
836 // could underflow if the underlying file is modified while reading
837 // it, but need to think about it more to determine if that's a
838 // real possibility
839 const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));
840
841 header.data_size = bmp_bytes_to_write;
842 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
843 try file.seekTo(bmp.file_header_len);
844 const file_reader = file.reader();
845 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.dib_header_size);
846 if (bitmap_info.getBitmasksByteLen() > 0) {
847 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.getBitmasksByteLen());
848 }
849 if (bitmap_info.getExpectedPaletteByteLen() > 0) {
850 try writeResourceDataNoPadding(writer, file_reader, @intCast(bitmap_info.getActualPaletteByteLen()));
851 // We know that the number of missing palette bytes is <= 4096
852 // (see `bmp_too_many_missing_palette_bytes` error case above)
853 const padding_bytes: usize = @intCast(bitmap_info.getMissingPaletteByteLen());
854 if (padding_bytes > 0) {
855 try writer.writeByteNTimes(0, padding_bytes);
856 }
857 }
858 try file.seekTo(bitmap_info.pixel_data_offset);
859 const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);
860 try writeResourceDataNoPadding(writer, file_reader, pixel_bytes);
861 try writeDataPadding(writer, bmp_bytes_to_write);
862 return;
863 },
864 .FONT => {
865 if (self.state.font_dir.ids.get(header.name_value.ordinal) != null) {
866 // Add warning and skip this resource
867 // Note: The Win32 compiler prints this as an error but it doesn't fail the compilation
868 // and the duplicate resource is skipped.
869 try self.addErrorDetails(ErrorDetails{
870 .err = .font_id_already_defined,
871 .token = node.id,
872 .type = .warning,
873 .extra = .{ .number = header.name_value.ordinal },
874 });
875 try self.addErrorDetails(ErrorDetails{
876 .err = .font_id_already_defined,
877 .token = self.state.font_dir.ids.get(header.name_value.ordinal).?,
878 .type = .note,
879 .extra = .{ .number = header.name_value.ordinal },
880 });
881 return;
882 }
883 header.applyMemoryFlags(node.common_resource_attributes, self.source);
884 const file_size = try file.getEndPos();
885 if (file_size > std.math.maxInt(u32)) {
886 return self.addErrorDetailsAndFail(.{
887 .err = .resource_data_size_exceeds_max,
888 .token = node.id,
889 });
890 }
891
892 // We now know that the data size will fit in a u32
893 header.data_size = @intCast(file_size);
894 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
895
896 var header_slurping_reader = headerSlurpingReader(148, file.reader());
897 try writeResourceData(writer, header_slurping_reader.reader(), header.data_size);
898
899 try self.state.font_dir.add(self.arena, FontDir.Font{
900 .id = header.name_value.ordinal,
901 .header_bytes = header_slurping_reader.slurped_header,
902 }, node.id);
903 return;
904 },
905 .ACCELERATOR,
906 .ANICURSOR,
907 .ANIICON,
908 .CURSOR,
909 .DIALOG,
910 .DLGINCLUDE,
911 .FONTDIR,
912 .ICON,
913 .MENU,
914 .STRING,
915 .TOOLBAR,
916 .VERSION,
917 .VXD,
918 => unreachable,
919 _ => unreachable,
920 }
921 } else {
922 header.applyMemoryFlags(node.common_resource_attributes, self.source);
923 }
924
925 // Fallback to just writing out the entire contents of the file
926 const data_size = try file.getEndPos();
927 if (data_size > std.math.maxInt(u32)) {
928 return self.addErrorDetailsAndFail(.{
929 .err = .resource_data_size_exceeds_max,
930 .token = node.id,
931 });
932 }
933 // We now know that the data size will fit in a u32
934 header.data_size = @intCast(data_size);
935 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
936 try writeResourceData(writer, file.reader(), header.data_size);
937 }
938
939 fn iconReadError(
940 self: *Compiler,
941 err: ico.ReadError,
942 filename: []const u8,
943 token: Token,
944 predefined_type: res.RT,
945 ) error{ CompileError, OutOfMemory } {
946 const filename_string_index = try self.diagnostics.putString(filename);
947 return self.addErrorDetailsAndFail(.{
948 .err = .icon_read_error,
949 .token = token,
950 .extra = .{ .icon_read_error = .{
951 .err = ErrorDetails.IconReadError.enumFromError(err),
952 .icon_type = switch (predefined_type) {
953 .GROUP_ICON => .icon,
954 .GROUP_CURSOR => .cursor,
955 else => unreachable,
956 },
957 .filename_string_index = filename_string_index,
958 } },
959 });
960 }
961
962 pub const DataType = enum {
963 number,
964 ascii_string,
965 wide_string,
966 };
967
968 pub const Data = union(DataType) {
969 number: Number,
970 ascii_string: []const u8,
971 wide_string: [:0]const u16,
972
973 pub fn deinit(self: Data, allocator: Allocator) void {
974 switch (self) {
975 .wide_string => |wide_string| {
976 allocator.free(wide_string);
977 },
978 .ascii_string => |ascii_string| {
979 allocator.free(ascii_string);
980 },
981 else => {},
982 }
983 }
984
985 pub fn write(self: Data, writer: anytype) !void {
986 switch (self) {
987 .number => |number| switch (number.is_long) {
988 false => try writer.writeInt(WORD, number.asWord(), .little),
989 true => try writer.writeInt(DWORD, number.value, .little),
990 },
991 .ascii_string => |ascii_string| {
992 try writer.writeAll(ascii_string);
993 },
994 .wide_string => |wide_string| {
995 try writer.writeAll(std.mem.sliceAsBytes(wide_string));
996 },
997 }
998 }
999 };
1000
1001 /// Assumes that the node is a number or number expression
1002 pub fn evaluateNumberExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) Number {
1003 switch (expression_node.id) {
1004 .literal => {
1005 const literal_node = expression_node.cast(.literal).?;
1006 std.debug.assert(literal_node.token.id == .number);
1007 const bytes = SourceBytes{
1008 .slice = literal_node.token.slice(source),
1009 .code_page = code_page_lookup.getForToken(literal_node.token),
1010 };
1011 return literals.parseNumberLiteral(bytes);
1012 },
1013 .binary_expression => {
1014 const binary_expression_node = expression_node.cast(.binary_expression).?;
1015 const lhs = evaluateNumberExpression(binary_expression_node.left, source, code_page_lookup);
1016 const rhs = evaluateNumberExpression(binary_expression_node.right, source, code_page_lookup);
1017 const operator_char = binary_expression_node.operator.slice(source)[0];
1018 return lhs.evaluateOperator(operator_char, rhs);
1019 },
1020 .grouped_expression => {
1021 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
1022 return evaluateNumberExpression(grouped_expression_node.expression, source, code_page_lookup);
1023 },
1024 else => unreachable,
1025 }
1026 }
1027
1028 const FlagsNumber = struct {
1029 value: u32,
1030 not_mask: u32 = 0xFFFFFFFF,
1031
1032 pub fn evaluateOperator(lhs: FlagsNumber, operator_char: u8, rhs: FlagsNumber) FlagsNumber {
1033 const result = switch (operator_char) {
1034 '-' => lhs.value -% rhs.value,
1035 '+' => lhs.value +% rhs.value,
1036 '|' => lhs.value | rhs.value,
1037 '&' => lhs.value & rhs.value,
1038 else => unreachable, // invalid operator, this would be a lexer/parser bug
1039 };
1040 return .{
1041 .value = result,
1042 .not_mask = lhs.not_mask & rhs.not_mask,
1043 };
1044 }
1045
1046 pub fn applyNotMask(self: FlagsNumber) u32 {
1047 return self.value & self.not_mask;
1048 }
1049 };
1050
1051 pub fn evaluateFlagsExpressionWithDefault(default: u32, expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) u32 {
1052 var context = FlagsExpressionContext{ .initial_value = default };
1053 const number = evaluateFlagsExpression(expression_node, source, code_page_lookup, &context);
1054 return number.value;
1055 }
1056
1057 pub const FlagsExpressionContext = struct {
1058 initial_value: u32 = 0,
1059 initial_value_used: bool = false,
1060 };
1061
1062 /// Assumes that the node is a number expression (which can contain not_expressions)
1063 pub fn evaluateFlagsExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup, context: *FlagsExpressionContext) FlagsNumber {
1064 switch (expression_node.id) {
1065 .literal => {
1066 const literal_node = expression_node.cast(.literal).?;
1067 std.debug.assert(literal_node.token.id == .number);
1068 const bytes = SourceBytes{
1069 .slice = literal_node.token.slice(source),
1070 .code_page = code_page_lookup.getForToken(literal_node.token),
1071 };
1072 var value = literals.parseNumberLiteral(bytes).value;
1073 if (!context.initial_value_used) {
1074 context.initial_value_used = true;
1075 value |= context.initial_value;
1076 }
1077 return .{ .value = value };
1078 },
1079 .binary_expression => {
1080 const binary_expression_node = expression_node.cast(.binary_expression).?;
1081 const lhs = evaluateFlagsExpression(binary_expression_node.left, source, code_page_lookup, context);
1082 const rhs = evaluateFlagsExpression(binary_expression_node.right, source, code_page_lookup, context);
1083 const operator_char = binary_expression_node.operator.slice(source)[0];
1084 const result = lhs.evaluateOperator(operator_char, rhs);
1085 return .{ .value = result.applyNotMask() };
1086 },
1087 .grouped_expression => {
1088 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
1089 return evaluateFlagsExpression(grouped_expression_node.expression, source, code_page_lookup, context);
1090 },
1091 .not_expression => {
1092 const not_expression = expression_node.cast(.not_expression).?;
1093 const bytes = SourceBytes{
1094 .slice = not_expression.number_token.slice(source),
1095 .code_page = code_page_lookup.getForToken(not_expression.number_token),
1096 };
1097 const not_number = literals.parseNumberLiteral(bytes);
1098 if (!context.initial_value_used) {
1099 context.initial_value_used = true;
1100 return .{ .value = context.initial_value & ~not_number.value };
1101 }
1102 return .{ .value = 0, .not_mask = ~not_number.value };
1103 },
1104 else => unreachable,
1105 }
1106 }
1107
1108 pub fn evaluateDataExpression(self: *Compiler, expression_node: *Node) !Data {
1109 switch (expression_node.id) {
1110 .literal => {
1111 const literal_node = expression_node.cast(.literal).?;
1112 switch (literal_node.token.id) {
1113 .number => {
1114 const number = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
1115 return .{ .number = number };
1116 },
1117 .quoted_ascii_string => {
1118 const column = literal_node.token.calculateColumn(self.source, 8, null);
1119 const bytes = SourceBytes{
1120 .slice = literal_node.token.slice(self.source),
1121 .code_page = self.input_code_pages.getForToken(literal_node.token),
1122 };
1123 const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{
1124 .start_column = column,
1125 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
1126 .output_code_page = self.output_code_pages.getForToken(literal_node.token),
1127 });
1128 errdefer self.allocator.free(parsed);
1129 return .{ .ascii_string = parsed };
1130 },
1131 .quoted_wide_string => {
1132 const column = literal_node.token.calculateColumn(self.source, 8, null);
1133 const bytes = SourceBytes{
1134 .slice = literal_node.token.slice(self.source),
1135 .code_page = self.input_code_pages.getForToken(literal_node.token),
1136 };
1137 const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{
1138 .start_column = column,
1139 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
1140 });
1141 errdefer self.allocator.free(parsed_string);
1142 return .{ .wide_string = parsed_string };
1143 },
1144 else => {
1145 std.debug.print("unexpected token in literal node: {}\n", .{literal_node.token});
1146 unreachable; // no other token types should be in a data literal node
1147 },
1148 }
1149 },
1150 .binary_expression, .grouped_expression => {
1151 const result = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
1152 return .{ .number = result };
1153 },
1154 .not_expression => unreachable,
1155 else => {
1156 std.debug.print("{}\n", .{expression_node.id});
1157 @panic("TODO: evaluateDataExpression");
1158 },
1159 }
1160 }
1161
1162 pub fn writeResourceRawData(self: *Compiler, node: *Node.ResourceRawData, writer: anytype) !void {
1163 var data_buffer = std.ArrayList(u8).init(self.allocator);
1164 defer data_buffer.deinit();
1165 // The header's data length field is a u32 so limit the resource's data size so that
1166 // we know we can always specify the real size.
1167 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1168 const data_writer = limited_writer.writer();
1169
1170 for (node.raw_data) |expression| {
1171 const data = try self.evaluateDataExpression(expression);
1172 defer data.deinit(self.allocator);
1173 data.write(data_writer) catch |err| switch (err) {
1174 error.NoSpaceLeft => {
1175 return self.addErrorDetailsAndFail(.{
1176 .err = .resource_data_size_exceeds_max,
1177 .token = node.id,
1178 });
1179 },
1180 else => |e| return e,
1181 };
1182 }
1183
1184 // This intCast can't fail because the limitedWriter above guarantees that
1185 // we will never write more than maxInt(u32) bytes.
1186 const data_len: u32 = @intCast(data_buffer.items.len);
1187 try self.writeResourceHeader(writer, node.id, node.type, data_len, node.common_resource_attributes, self.state.language);
1188
1189 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1190 try writeResourceData(writer, data_fbs.reader(), data_len);
1191 }
1192
1193 pub fn writeResourceHeader(self: *Compiler, writer: anytype, id_token: Token, type_token: Token, data_size: u32, common_resource_attributes: []Token, language: res.Language) !void {
1194 var header = try self.resourceHeader(id_token, type_token, .{
1195 .language = language,
1196 .data_size = data_size,
1197 });
1198 defer header.deinit(self.allocator);
1199
1200 header.applyMemoryFlags(common_resource_attributes, self.source);
1201
1202 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = id_token });
1203 }
1204
1205 pub fn writeResourceDataNoPadding(writer: anytype, data_reader: anytype, data_size: u32) !void {
1206 var limited_reader = std.io.limitedReader(data_reader, data_size);
1207
1208 const FifoBuffer = std.fifo.LinearFifo(u8, .{ .Static = 4096 });
1209 var fifo = FifoBuffer.init();
1210 try fifo.pump(limited_reader.reader(), writer);
1211 }
1212
1213 pub fn writeResourceData(writer: anytype, data_reader: anytype, data_size: u32) !void {
1214 try writeResourceDataNoPadding(writer, data_reader, data_size);
1215 try writeDataPadding(writer, data_size);
1216 }
1217
1218 pub fn writeDataPadding(writer: anytype, data_size: u32) !void {
1219 try writer.writeByteNTimes(0, numPaddingBytesNeeded(data_size));
1220 }
1221
1222 pub fn numPaddingBytesNeeded(data_size: u32) u2 {
1223 // Result is guaranteed to be between 0 and 3.
1224 return @intCast((4 -% data_size) % 4);
1225 }
1226
1227 pub fn evaluateAcceleratorKeyExpression(self: *Compiler, node: *Node, is_virt: bool) !u16 {
1228 if (node.isNumberExpression()) {
1229 return evaluateNumberExpression(node, self.source, self.input_code_pages).asWord();
1230 } else {
1231 std.debug.assert(node.isStringLiteral());
1232 const literal = @fieldParentPtr(Node.Literal, "base", node);
1233 const bytes = SourceBytes{
1234 .slice = literal.token.slice(self.source),
1235 .code_page = self.input_code_pages.getForToken(literal.token),
1236 };
1237 const column = literal.token.calculateColumn(self.source, 8, null);
1238 return res.parseAcceleratorKeyString(bytes, is_virt, .{
1239 .start_column = column,
1240 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal.token },
1241 });
1242 }
1243 }
1244
1245 pub fn writeAccelerators(self: *Compiler, node: *Node.Accelerators, writer: anytype) !void {
1246 var data_buffer = std.ArrayList(u8).init(self.allocator);
1247 defer data_buffer.deinit();
1248
1249 // The header's data length field is a u32 so limit the resource's data size so that
1250 // we know we can always specify the real size.
1251 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1252 const data_writer = limited_writer.writer();
1253
1254 self.writeAcceleratorsData(node, data_writer) catch |err| switch (err) {
1255 error.NoSpaceLeft => {
1256 return self.addErrorDetailsAndFail(.{
1257 .err = .resource_data_size_exceeds_max,
1258 .token = node.id,
1259 });
1260 },
1261 else => |e| return e,
1262 };
1263
1264 // This intCast can't fail because the limitedWriter above guarantees that
1265 // we will never write more than maxInt(u32) bytes.
1266 const data_size: u32 = @intCast(data_buffer.items.len);
1267 var header = try self.resourceHeader(node.id, node.type, .{
1268 .data_size = data_size,
1269 });
1270 defer header.deinit(self.allocator);
1271
1272 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1273 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
1274
1275 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1276
1277 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1278 try writeResourceData(writer, data_fbs.reader(), data_size);
1279 }
1280
1281 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
1282 /// the writer within this function could return error.NoSpaceLeft
1283 pub fn writeAcceleratorsData(self: *Compiler, node: *Node.Accelerators, data_writer: anytype) !void {
1284 for (node.accelerators, 0..) |accel_node, i| {
1285 const accelerator = @fieldParentPtr(Node.Accelerator, "base", accel_node);
1286 var modifiers = res.AcceleratorModifiers{};
1287 for (accelerator.type_and_options) |type_or_option| {
1288 const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;
1289 modifiers.apply(modifier);
1290 }
1291 if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {
1292 return self.addErrorDetailsAndFail(.{
1293 .err = .accelerator_type_required,
1294 .token = accelerator.event.getFirstToken(),
1295 .token_span_end = accelerator.event.getLastToken(),
1296 });
1297 }
1298 const key = self.evaluateAcceleratorKeyExpression(accelerator.event, modifiers.isSet(.virtkey)) catch |err| switch (err) {
1299 error.OutOfMemory => |e| return e,
1300 else => |e| {
1301 return self.addErrorDetailsAndFail(.{
1302 .err = .invalid_accelerator_key,
1303 .token = accelerator.event.getFirstToken(),
1304 .token_span_end = accelerator.event.getLastToken(),
1305 .extra = .{ .accelerator_error = .{
1306 .err = ErrorDetails.AcceleratorError.enumFromError(e),
1307 } },
1308 });
1309 },
1310 };
1311 const cmd_id = evaluateNumberExpression(accelerator.idvalue, self.source, self.input_code_pages);
1312
1313 if (i == node.accelerators.len - 1) {
1314 modifiers.markLast();
1315 }
1316
1317 try data_writer.writeByte(modifiers.value);
1318 try data_writer.writeByte(0); // padding
1319 try data_writer.writeInt(u16, key, .little);
1320 try data_writer.writeInt(u16, cmd_id.asWord(), .little);
1321 try data_writer.writeInt(u16, 0, .little); // padding
1322 }
1323 }
1324
1325 const DialogOptionalStatementValues = struct {
1326 style: u32 = res.WS.SYSMENU | res.WS.BORDER | res.WS.POPUP,
1327 exstyle: u32 = 0,
1328 class: ?NameOrOrdinal = null,
1329 menu: ?NameOrOrdinal = null,
1330 font: ?FontStatementValues = null,
1331 caption: ?Token = null,
1332 };
1333
1334 pub fn writeDialog(self: *Compiler, node: *Node.Dialog, writer: anytype) !void {
1335 var data_buffer = std.ArrayList(u8).init(self.allocator);
1336 defer data_buffer.deinit();
1337 // The header's data length field is a u32 so limit the resource's data size so that
1338 // we know we can always specify the real size.
1339 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1340 const data_writer = limited_writer.writer();
1341
1342 const resource = Resource.fromString(.{
1343 .slice = node.type.slice(self.source),
1344 .code_page = self.input_code_pages.getForToken(node.type),
1345 });
1346 std.debug.assert(resource == .dialog or resource == .dialogex);
1347
1348 var optional_statement_values: DialogOptionalStatementValues = .{};
1349 defer {
1350 if (optional_statement_values.class) |class| {
1351 class.deinit(self.allocator);
1352 }
1353 if (optional_statement_values.menu) |menu| {
1354 menu.deinit(self.allocator);
1355 }
1356 }
1357 var skipped_menu_or_classes = std.ArrayList(*Node.SimpleStatement).init(self.allocator);
1358 defer skipped_menu_or_classes.deinit();
1359 var last_menu: *Node.SimpleStatement = undefined;
1360 var last_class: *Node.SimpleStatement = undefined;
1361 var last_menu_would_be_forced_ordinal = false;
1362 var last_menu_has_digit_as_first_char = false;
1363 var last_menu_did_uppercase = false;
1364 var last_class_would_be_forced_ordinal = false;
1365
1366 for (node.optional_statements) |optional_statement| {
1367 switch (optional_statement.id) {
1368 .simple_statement => {
1369 const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", optional_statement);
1370 const statement_identifier = simple_statement.identifier;
1371 const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
1372 switch (statement_type) {
1373 .style, .exstyle => {
1374 const style = evaluateFlagsExpressionWithDefault(0, simple_statement.value, self.source, self.input_code_pages);
1375 if (statement_type == .style) {
1376 optional_statement_values.style = style;
1377 } else {
1378 optional_statement_values.exstyle = style;
1379 }
1380 },
1381 .caption => {
1382 std.debug.assert(simple_statement.value.id == .literal);
1383 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1384 optional_statement_values.caption = literal_node.token;
1385 },
1386 .class => {
1387 const is_duplicate = optional_statement_values.class != null;
1388 if (is_duplicate) {
1389 try skipped_menu_or_classes.append(last_class);
1390 }
1391 const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;
1392 // In the Win32 RC compiler, if any CLASS values that are interpreted as
1393 // an ordinal exist, it affects all future CLASS statements and forces
1394 // them to be treated as an ordinal no matter what.
1395 if (forced_ordinal) {
1396 last_class_would_be_forced_ordinal = true;
1397 }
1398 // clear out the old one if it exists
1399 if (optional_statement_values.class) |prev| {
1400 prev.deinit(self.allocator);
1401 optional_statement_values.class = null;
1402 }
1403
1404 if (simple_statement.value.isNumberExpression()) {
1405 const class_ordinal = evaluateNumberExpression(simple_statement.value, self.source, self.input_code_pages);
1406 optional_statement_values.class = NameOrOrdinal{ .ordinal = class_ordinal.asWord() };
1407 } else {
1408 std.debug.assert(simple_statement.value.isStringLiteral());
1409 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1410 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
1411 optional_statement_values.class = NameOrOrdinal{ .name = parsed };
1412 }
1413
1414 last_class = simple_statement;
1415 },
1416 .menu => {
1417 const is_duplicate = optional_statement_values.menu != null;
1418 if (is_duplicate) {
1419 try skipped_menu_or_classes.append(last_menu);
1420 }
1421 const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;
1422 // In the Win32 RC compiler, if any MENU values that are interpreted as
1423 // an ordinal exist, it affects all future MENU statements and forces
1424 // them to be treated as an ordinal no matter what.
1425 if (forced_ordinal) {
1426 last_menu_would_be_forced_ordinal = true;
1427 }
1428 // clear out the old one if it exists
1429 if (optional_statement_values.menu) |prev| {
1430 prev.deinit(self.allocator);
1431 optional_statement_values.menu = null;
1432 }
1433
1434 std.debug.assert(simple_statement.value.id == .literal);
1435 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
1436
1437 const token_slice = literal_node.token.slice(self.source);
1438 const bytes = SourceBytes{
1439 .slice = token_slice,
1440 .code_page = self.input_code_pages.getForToken(literal_node.token),
1441 };
1442 optional_statement_values.menu = try NameOrOrdinal.fromString(self.allocator, bytes);
1443
1444 if (optional_statement_values.menu.? == .name) {
1445 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(bytes)) |win32_rc_ordinal| {
1446 try self.addErrorDetails(.{
1447 .err = .invalid_digit_character_in_ordinal,
1448 .type = .err,
1449 .token = literal_node.token,
1450 });
1451 return self.addErrorDetailsAndFail(.{
1452 .err = .win32_non_ascii_ordinal,
1453 .type = .note,
1454 .token = literal_node.token,
1455 .print_source_line = false,
1456 .extra = .{ .number = win32_rc_ordinal.ordinal },
1457 });
1458 }
1459 }
1460
1461 // Need to keep track of some properties of the value
1462 // in order to emit the appropriate warning(s) later on.
1463 // See where the warning are emitted below (outside this loop)
1464 // for the full explanation.
1465 var did_uppercase = false;
1466 var codepoint_i: usize = 0;
1467 while (bytes.code_page.codepointAt(codepoint_i, bytes.slice)) |codepoint| : (codepoint_i += codepoint.byte_len) {
1468 const c = codepoint.value;
1469 switch (c) {
1470 'a'...'z' => {
1471 did_uppercase = true;
1472 break;
1473 },
1474 else => {},
1475 }
1476 }
1477 last_menu_did_uppercase = did_uppercase;
1478 last_menu_has_digit_as_first_char = std.ascii.isDigit(token_slice[0]);
1479 last_menu = simple_statement;
1480 },
1481 else => {},
1482 }
1483 },
1484 .font_statement => {
1485 const font = @fieldParentPtr(Node.FontStatement, "base", optional_statement);
1486 if (optional_statement_values.font != null) {
1487 optional_statement_values.font.?.node = font;
1488 } else {
1489 optional_statement_values.font = FontStatementValues{ .node = font };
1490 }
1491 if (font.weight) |weight| {
1492 const value = evaluateNumberExpression(weight, self.source, self.input_code_pages);
1493 optional_statement_values.font.?.weight = value.asWord();
1494 }
1495 if (font.italic) |italic| {
1496 const value = evaluateNumberExpression(italic, self.source, self.input_code_pages);
1497 optional_statement_values.font.?.italic = value.asWord() != 0;
1498 }
1499 },
1500 else => {},
1501 }
1502 }
1503
1504 for (skipped_menu_or_classes.items) |simple_statement| {
1505 const statement_identifier = simple_statement.identifier;
1506 const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
1507 try self.addErrorDetails(.{
1508 .err = .duplicate_menu_or_class_skipped,
1509 .type = .warning,
1510 .token = simple_statement.identifier,
1511 .token_span_start = simple_statement.base.getFirstToken(),
1512 .token_span_end = simple_statement.base.getLastToken(),
1513 .extra = .{ .menu_or_class = switch (statement_type) {
1514 .menu => .menu,
1515 .class => .class,
1516 else => unreachable,
1517 } },
1518 });
1519 }
1520 // The Win32 RC compiler miscompiles the value in the following scenario:
1521 // Multiple CLASS parameters are specified and any of them are treated as a number, then
1522 // the last CLASS is always treated as a number no matter what
1523 if (last_class_would_be_forced_ordinal and optional_statement_values.class.? == .name) {
1524 const literal_node = @fieldParentPtr(Node.Literal, "base", last_class.value);
1525 const ordinal_value = res.ForcedOrdinal.fromUtf16Le(optional_statement_values.class.?.name);
1526
1527 try self.addErrorDetails(.{
1528 .err = .rc_would_miscompile_dialog_class,
1529 .type = .warning,
1530 .token = literal_node.token,
1531 .extra = .{ .number = ordinal_value },
1532 });
1533 try self.addErrorDetails(.{
1534 .err = .rc_would_miscompile_dialog_class,
1535 .type = .note,
1536 .print_source_line = false,
1537 .token = literal_node.token,
1538 .extra = .{ .number = ordinal_value },
1539 });
1540 try self.addErrorDetails(.{
1541 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
1542 .type = .note,
1543 .print_source_line = false,
1544 .token = literal_node.token,
1545 .extra = .{ .menu_or_class = .class },
1546 });
1547 }
1548 // The Win32 RC compiler miscompiles the id in two different scenarios:
1549 // 1. The first character of the ID is a digit, in which case it is always treated as a number
1550 // no matter what (and therefore does not match how the MENU/MENUEX id is parsed)
1551 // 2. Multiple MENU parameters are specified and any of them are treated as a number, then
1552 // the last MENU is always treated as a number no matter what
1553 if ((last_menu_would_be_forced_ordinal or last_menu_has_digit_as_first_char) and optional_statement_values.menu.? == .name) {
1554 const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
1555 const token_slice = literal_node.token.slice(self.source);
1556 const bytes = SourceBytes{
1557 .slice = token_slice,
1558 .code_page = self.input_code_pages.getForToken(literal_node.token),
1559 };
1560 const ordinal_value = res.ForcedOrdinal.fromBytes(bytes);
1561
1562 try self.addErrorDetails(.{
1563 .err = .rc_would_miscompile_dialog_menu_id,
1564 .type = .warning,
1565 .token = literal_node.token,
1566 .extra = .{ .number = ordinal_value },
1567 });
1568 try self.addErrorDetails(.{
1569 .err = .rc_would_miscompile_dialog_menu_id,
1570 .type = .note,
1571 .print_source_line = false,
1572 .token = literal_node.token,
1573 .extra = .{ .number = ordinal_value },
1574 });
1575 if (last_menu_would_be_forced_ordinal) {
1576 try self.addErrorDetails(.{
1577 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
1578 .type = .note,
1579 .print_source_line = false,
1580 .token = literal_node.token,
1581 .extra = .{ .menu_or_class = .menu },
1582 });
1583 } else {
1584 try self.addErrorDetails(.{
1585 .err = .rc_would_miscompile_dialog_menu_id_starts_with_digit,
1586 .type = .note,
1587 .print_source_line = false,
1588 .token = literal_node.token,
1589 });
1590 }
1591 }
1592 // The MENU id parsing uses the exact same logic as the MENU/MENUEX resource id parsing,
1593 // which means that it will convert ASCII characters to uppercase during the 'name' parsing.
1594 // This turns out not to matter (`LoadMenu` does a case-insensitive lookup anyway),
1595 // but it still makes sense to share the uppercasing logic since the MENU parameter
1596 // here is just a reference to a MENU/MENUEX id within the .exe.
1597 // So, because this is an intentional but inconsequential-to-the-user difference
1598 // between resinator and the Win32 RC compiler, we only emit a hint instead of
1599 // a warning.
1600 if (last_menu_did_uppercase) {
1601 const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
1602 try self.addErrorDetails(.{
1603 .err = .dialog_menu_id_was_uppercased,
1604 .type = .hint,
1605 .token = literal_node.token,
1606 });
1607 }
1608
1609 const x = evaluateNumberExpression(node.x, self.source, self.input_code_pages);
1610 const y = evaluateNumberExpression(node.y, self.source, self.input_code_pages);
1611 const width = evaluateNumberExpression(node.width, self.source, self.input_code_pages);
1612 const height = evaluateNumberExpression(node.height, self.source, self.input_code_pages);
1613
1614 // FONT statement requires DS_SETFONT, and if it's not present DS_SETFRONT must be unset
1615 if (optional_statement_values.font) |_| {
1616 optional_statement_values.style |= res.DS.SETFONT;
1617 } else {
1618 optional_statement_values.style &= ~res.DS.SETFONT;
1619 }
1620 // CAPTION statement implies WS_CAPTION
1621 if (optional_statement_values.caption) |_| {
1622 optional_statement_values.style |= res.WS.CAPTION;
1623 }
1624
1625 self.writeDialogHeaderAndStrings(
1626 node,
1627 data_writer,
1628 resource,
1629 &optional_statement_values,
1630 x,
1631 y,
1632 width,
1633 height,
1634 ) catch |err| switch (err) {
1635 // Dialog header and menu/class/title strings can never exceed u32 bytes
1636 // on their own, so this error is unreachable.
1637 error.NoSpaceLeft => unreachable,
1638 else => |e| return e,
1639 };
1640
1641 var controls_by_id = std.AutoHashMap(u32, *const Node.ControlStatement).init(self.allocator);
1642 // Number of controls are guaranteed by the parser to be within maxInt(u16).
1643 try controls_by_id.ensureTotalCapacity(@as(u16, @intCast(node.controls.len)));
1644 defer controls_by_id.deinit();
1645
1646 for (node.controls) |control_node| {
1647 const control = @fieldParentPtr(Node.ControlStatement, "base", control_node);
1648
1649 self.writeDialogControl(
1650 control,
1651 data_writer,
1652 resource,
1653 // We know the data_buffer len is limited to u32 max.
1654 @intCast(data_buffer.items.len),
1655 &controls_by_id,
1656 ) catch |err| switch (err) {
1657 error.NoSpaceLeft => {
1658 try self.addErrorDetails(.{
1659 .err = .resource_data_size_exceeds_max,
1660 .token = node.id,
1661 });
1662 return self.addErrorDetailsAndFail(.{
1663 .err = .resource_data_size_exceeds_max,
1664 .type = .note,
1665 .token = control.type,
1666 });
1667 },
1668 else => |e| return e,
1669 };
1670 }
1671
1672 const data_size: u32 = @intCast(data_buffer.items.len);
1673 var header = try self.resourceHeader(node.id, node.type, .{
1674 .data_size = data_size,
1675 });
1676 defer header.deinit(self.allocator);
1677
1678 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1679 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
1680
1681 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1682
1683 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1684 try writeResourceData(writer, data_fbs.reader(), data_size);
1685 }
1686
1687 fn writeDialogHeaderAndStrings(
1688 self: *Compiler,
1689 node: *Node.Dialog,
1690 data_writer: anytype,
1691 resource: Resource,
1692 optional_statement_values: *const DialogOptionalStatementValues,
1693 x: Number,
1694 y: Number,
1695 width: Number,
1696 height: Number,
1697 ) !void {
1698 // Header
1699 if (resource == .dialogex) {
1700 const help_id: u32 = help_id: {
1701 if (node.help_id == null) break :help_id 0;
1702 break :help_id evaluateNumberExpression(node.help_id.?, self.source, self.input_code_pages).value;
1703 };
1704 try data_writer.writeInt(u16, 1, .little); // version number, always 1
1705 try data_writer.writeInt(u16, 0xFFFF, .little); // signature, always 0xFFFF
1706 try data_writer.writeInt(u32, help_id, .little);
1707 try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
1708 try data_writer.writeInt(u32, optional_statement_values.style, .little);
1709 } else {
1710 try data_writer.writeInt(u32, optional_statement_values.style, .little);
1711 try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
1712 }
1713 // This limit is enforced by the parser, so we know the number of controls
1714 // is within the range of a u16.
1715 try data_writer.writeInt(u16, @as(u16, @intCast(node.controls.len)), .little);
1716 try data_writer.writeInt(u16, x.asWord(), .little);
1717 try data_writer.writeInt(u16, y.asWord(), .little);
1718 try data_writer.writeInt(u16, width.asWord(), .little);
1719 try data_writer.writeInt(u16, height.asWord(), .little);
1720
1721 // Menu
1722 if (optional_statement_values.menu) |menu| {
1723 try menu.write(data_writer);
1724 } else {
1725 try data_writer.writeInt(u16, 0, .little);
1726 }
1727 // Class
1728 if (optional_statement_values.class) |class| {
1729 try class.write(data_writer);
1730 } else {
1731 try data_writer.writeInt(u16, 0, .little);
1732 }
1733 // Caption
1734 if (optional_statement_values.caption) |caption| {
1735 const parsed = try self.parseQuotedStringAsWideString(caption);
1736 defer self.allocator.free(parsed);
1737 try data_writer.writeAll(std.mem.sliceAsBytes(parsed[0 .. parsed.len + 1]));
1738 } else {
1739 try data_writer.writeInt(u16, 0, .little);
1740 }
1741 // Font
1742 if (optional_statement_values.font) |font| {
1743 try self.writeDialogFont(resource, font, data_writer);
1744 }
1745 }
1746
1747 fn writeDialogControl(
1748 self: *Compiler,
1749 control: *Node.ControlStatement,
1750 data_writer: anytype,
1751 resource: Resource,
1752 bytes_written_so_far: u32,
1753 controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),
1754 ) !void {
1755 const control_type = rc.Control.map.get(control.type.slice(self.source)).?;
1756
1757 // Each control must be at a 4-byte boundary. However, the Windows RC
1758 // compiler will miscompile controls if their extra data ends on an odd offset.
1759 // We will avoid the miscompilation and emit a warning.
1760 const num_padding = numPaddingBytesNeeded(bytes_written_so_far);
1761 if (num_padding == 1 or num_padding == 3) {
1762 try self.addErrorDetails(.{
1763 .err = .rc_would_miscompile_control_padding,
1764 .type = .warning,
1765 .token = control.type,
1766 });
1767 try self.addErrorDetails(.{
1768 .err = .rc_would_miscompile_control_padding,
1769 .type = .note,
1770 .print_source_line = false,
1771 .token = control.type,
1772 });
1773 }
1774 try data_writer.writeByteNTimes(0, num_padding);
1775
1776 const style = if (control.style) |style_expression|
1777 // Certain styles are implied by the control type
1778 evaluateFlagsExpressionWithDefault(res.ControlClass.getImpliedStyle(control_type), style_expression, self.source, self.input_code_pages)
1779 else
1780 res.ControlClass.getImpliedStyle(control_type);
1781
1782 const exstyle = if (control.exstyle) |exstyle_expression|
1783 evaluateFlagsExpressionWithDefault(0, exstyle_expression, self.source, self.input_code_pages)
1784 else
1785 0;
1786
1787 switch (resource) {
1788 .dialog => {
1789 // Note: Reverse order from DIALOGEX
1790 try data_writer.writeInt(u32, style, .little);
1791 try data_writer.writeInt(u32, exstyle, .little);
1792 },
1793 .dialogex => {
1794 const help_id: u32 = if (control.help_id) |help_id_expression|
1795 evaluateNumberExpression(help_id_expression, self.source, self.input_code_pages).value
1796 else
1797 0;
1798 try data_writer.writeInt(u32, help_id, .little);
1799 // Note: Reverse order from DIALOG
1800 try data_writer.writeInt(u32, exstyle, .little);
1801 try data_writer.writeInt(u32, style, .little);
1802 },
1803 else => unreachable,
1804 }
1805
1806 const control_x = evaluateNumberExpression(control.x, self.source, self.input_code_pages);
1807 const control_y = evaluateNumberExpression(control.y, self.source, self.input_code_pages);
1808 const control_width = evaluateNumberExpression(control.width, self.source, self.input_code_pages);
1809 const control_height = evaluateNumberExpression(control.height, self.source, self.input_code_pages);
1810
1811 try data_writer.writeInt(u16, control_x.asWord(), .little);
1812 try data_writer.writeInt(u16, control_y.asWord(), .little);
1813 try data_writer.writeInt(u16, control_width.asWord(), .little);
1814 try data_writer.writeInt(u16, control_height.asWord(), .little);
1815
1816 const control_id = evaluateNumberExpression(control.id, self.source, self.input_code_pages);
1817 switch (resource) {
1818 .dialog => try data_writer.writeInt(u16, control_id.asWord(), .little),
1819 .dialogex => try data_writer.writeInt(u32, control_id.value, .little),
1820 else => unreachable,
1821 }
1822
1823 const control_id_for_map: u32 = switch (resource) {
1824 .dialog => control_id.asWord(),
1825 .dialogex => control_id.value,
1826 else => unreachable,
1827 };
1828 const result = controls_by_id.getOrPutAssumeCapacity(control_id_for_map);
1829 if (result.found_existing) {
1830 if (!self.silent_duplicate_control_ids) {
1831 try self.addErrorDetails(.{
1832 .err = .control_id_already_defined,
1833 .type = .warning,
1834 .token = control.id.getFirstToken(),
1835 .token_span_end = control.id.getLastToken(),
1836 .extra = .{ .number = control_id_for_map },
1837 });
1838 try self.addErrorDetails(.{
1839 .err = .control_id_already_defined,
1840 .type = .note,
1841 .token = result.value_ptr.*.id.getFirstToken(),
1842 .token_span_end = result.value_ptr.*.id.getLastToken(),
1843 .extra = .{ .number = control_id_for_map },
1844 });
1845 }
1846 } else {
1847 result.value_ptr.* = control;
1848 }
1849
1850 if (res.ControlClass.fromControl(control_type)) |control_class| {
1851 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1852 try ordinal.write(data_writer);
1853 } else {
1854 const class_node = control.class.?;
1855 if (class_node.isNumberExpression()) {
1856 const number = evaluateNumberExpression(class_node, self.source, self.input_code_pages);
1857 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
1858 // This is different from how the Windows RC compiles ordinals here,
1859 // but I think that's a miscompilation/bug of the Windows implementation.
1860 // The Windows behavior is (where LSB = least significant byte):
1861 // - If the LSB is 0x00 => 0xFFFF0000
1862 // - If the LSB is < 0x80 => 0x000000<LSB>
1863 // - If the LSB is >= 0x80 => 0x0000FF<LSB>
1864 //
1865 // Because of this, we emit a warning about the potential miscompilation
1866 try self.addErrorDetails(.{
1867 .err = .rc_would_miscompile_control_class_ordinal,
1868 .type = .warning,
1869 .token = class_node.getFirstToken(),
1870 .token_span_end = class_node.getLastToken(),
1871 });
1872 try self.addErrorDetails(.{
1873 .err = .rc_would_miscompile_control_class_ordinal,
1874 .type = .note,
1875 .print_source_line = false,
1876 .token = class_node.getFirstToken(),
1877 .token_span_end = class_node.getLastToken(),
1878 });
1879 // And then write out the ordinal using a proper a NameOrOrdinal encoding.
1880 try ordinal.write(data_writer);
1881 } else if (class_node.isStringLiteral()) {
1882 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
1883 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
1884 defer self.allocator.free(parsed);
1885 if (rc.ControlClass.fromWideString(parsed)) |control_class| {
1886 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1887 try ordinal.write(data_writer);
1888 } else {
1889 // NUL acts as a terminator
1890 // TODO: Maybe warn when parsed_terminated.len != parsed.len, since
1891 // it seems unlikely that NUL-termination is something intentional
1892 const parsed_terminated = std.mem.sliceTo(parsed, 0);
1893 const name = NameOrOrdinal{ .name = parsed_terminated };
1894 try name.write(data_writer);
1895 }
1896 } else {
1897 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
1898 const literal_slice = literal_node.token.slice(self.source);
1899 // This succeeding is guaranteed by the parser
1900 const control_class = rc.ControlClass.map.get(literal_slice) orelse unreachable;
1901 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
1902 try ordinal.write(data_writer);
1903 }
1904 }
1905
1906 if (control.text) |text_token| {
1907 const bytes = SourceBytes{
1908 .slice = text_token.slice(self.source),
1909 .code_page = self.input_code_pages.getForToken(text_token),
1910 };
1911 if (text_token.isStringLiteral()) {
1912 const text = try self.parseQuotedStringAsWideString(text_token);
1913 defer self.allocator.free(text);
1914 const name = NameOrOrdinal{ .name = text };
1915 try name.write(data_writer);
1916 } else {
1917 std.debug.assert(text_token.id == .number);
1918 const number = literals.parseNumberLiteral(bytes);
1919 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
1920 try ordinal.write(data_writer);
1921 }
1922 } else {
1923 try NameOrOrdinal.writeEmpty(data_writer);
1924 }
1925
1926 var extra_data_buf = std.ArrayList(u8).init(self.allocator);
1927 defer extra_data_buf.deinit();
1928 // The extra data byte length must be able to fit within a u16.
1929 var limited_extra_data_writer = limitedWriter(extra_data_buf.writer(), std.math.maxInt(u16));
1930 const extra_data_writer = limited_extra_data_writer.writer();
1931 for (control.extra_data) |data_expression| {
1932 const data = try self.evaluateDataExpression(data_expression);
1933 defer data.deinit(self.allocator);
1934 data.write(extra_data_writer) catch |err| switch (err) {
1935 error.NoSpaceLeft => {
1936 try self.addErrorDetails(.{
1937 .err = .control_extra_data_size_exceeds_max,
1938 .token = control.type,
1939 });
1940 return self.addErrorDetailsAndFail(.{
1941 .err = .control_extra_data_size_exceeds_max,
1942 .type = .note,
1943 .token = data_expression.getFirstToken(),
1944 .token_span_end = data_expression.getLastToken(),
1945 });
1946 },
1947 else => |e| return e,
1948 };
1949 }
1950 // We know the extra_data_buf size fits within a u16.
1951 const extra_data_size: u16 = @intCast(extra_data_buf.items.len);
1952 try data_writer.writeInt(u16, extra_data_size, .little);
1953 try data_writer.writeAll(extra_data_buf.items);
1954 }
1955
1956 pub fn writeToolbar(self: *Compiler, node: *Node.Toolbar, writer: anytype) !void {
1957 var data_buffer = std.ArrayList(u8).init(self.allocator);
1958 defer data_buffer.deinit();
1959 const data_writer = data_buffer.writer();
1960
1961 const button_width = evaluateNumberExpression(node.button_width, self.source, self.input_code_pages);
1962 const button_height = evaluateNumberExpression(node.button_height, self.source, self.input_code_pages);
1963
1964 // I'm assuming this is some sort of version
1965 // TODO: Try to find something mentioning this
1966 try data_writer.writeInt(u16, 1, .little);
1967 try data_writer.writeInt(u16, button_width.asWord(), .little);
1968 try data_writer.writeInt(u16, button_height.asWord(), .little);
1969 try data_writer.writeInt(u16, @as(u16, @intCast(node.buttons.len)), .little);
1970
1971 for (node.buttons) |button_or_sep| {
1972 switch (button_or_sep.id) {
1973 .literal => { // This is always SEPARATOR
1974 std.debug.assert(button_or_sep.cast(.literal).?.token.id == .literal);
1975 try data_writer.writeInt(u16, 0, .little);
1976 },
1977 .simple_statement => {
1978 const value_node = button_or_sep.cast(.simple_statement).?.value;
1979 const value = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
1980 try data_writer.writeInt(u16, value.asWord(), .little);
1981 },
1982 else => unreachable, // This is a bug in the parser
1983 }
1984 }
1985
1986 const data_size: u32 = @intCast(data_buffer.items.len);
1987 var header = try self.resourceHeader(node.id, node.type, .{
1988 .data_size = data_size,
1989 });
1990 defer header.deinit(self.allocator);
1991
1992 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1993
1994 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
1995
1996 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1997 try writeResourceData(writer, data_fbs.reader(), data_size);
1998 }
1999
2000 /// Weight and italic carry over from previous FONT statements within a single resource,
2001 /// so they need to be parsed ahead-of-time and stored
2002 const FontStatementValues = struct {
2003 weight: u16 = 0,
2004 italic: bool = false,
2005 node: *Node.FontStatement,
2006 };
2007
2008 pub fn writeDialogFont(self: *Compiler, resource: Resource, values: FontStatementValues, writer: anytype) !void {
2009 const node = values.node;
2010 const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);
2011 try writer.writeInt(u16, point_size.asWord(), .little);
2012
2013 if (resource == .dialogex) {
2014 try writer.writeInt(u16, values.weight, .little);
2015 }
2016
2017 if (resource == .dialogex) {
2018 try writer.writeInt(u8, @intFromBool(values.italic), .little);
2019 }
2020
2021 if (node.char_set) |char_set| {
2022 const value = evaluateNumberExpression(char_set, self.source, self.input_code_pages);
2023 try writer.writeInt(u8, @as(u8, @truncate(value.value)), .little);
2024 } else if (resource == .dialogex) {
2025 try writer.writeInt(u8, 1, .little); // DEFAULT_CHARSET
2026 }
2027
2028 const typeface = try self.parseQuotedStringAsWideString(node.typeface);
2029 defer self.allocator.free(typeface);
2030 try writer.writeAll(std.mem.sliceAsBytes(typeface[0 .. typeface.len + 1]));
2031 }
2032
2033 pub fn writeMenu(self: *Compiler, node: *Node.Menu, writer: anytype) !void {
2034 var data_buffer = std.ArrayList(u8).init(self.allocator);
2035 defer data_buffer.deinit();
2036 // The header's data length field is a u32 so limit the resource's data size so that
2037 // we know we can always specify the real size.
2038 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
2039 const data_writer = limited_writer.writer();
2040
2041 const type_bytes = SourceBytes{
2042 .slice = node.type.slice(self.source),
2043 .code_page = self.input_code_pages.getForToken(node.type),
2044 };
2045 const resource = Resource.fromString(type_bytes);
2046 std.debug.assert(resource == .menu or resource == .menuex);
2047
2048 self.writeMenuData(node, data_writer, resource) catch |err| switch (err) {
2049 error.NoSpaceLeft => {
2050 return self.addErrorDetailsAndFail(.{
2051 .err = .resource_data_size_exceeds_max,
2052 .token = node.id,
2053 });
2054 },
2055 else => |e| return e,
2056 };
2057
2058 // This intCast can't fail because the limitedWriter above guarantees that
2059 // we will never write more than maxInt(u32) bytes.
2060 const data_size: u32 = @intCast(data_buffer.items.len);
2061 var header = try self.resourceHeader(node.id, node.type, .{
2062 .data_size = data_size,
2063 });
2064 defer header.deinit(self.allocator);
2065
2066 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2067 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
2068
2069 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
2070
2071 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2072 try writeResourceData(writer, data_fbs.reader(), data_size);
2073 }
2074
2075 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
2076 /// the writer within this function could return error.NoSpaceLeft
2077 pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: anytype, resource: Resource) !void {
2078 // menu header
2079 const version: u16 = if (resource == .menu) 0 else 1;
2080 try data_writer.writeInt(u16, version, .little);
2081 const header_size: u16 = if (resource == .menu) 0 else 4;
2082 try data_writer.writeInt(u16, header_size, .little); // cbHeaderSize
2083 // Note: There can be extra bytes at the end of this header (`rgbExtra`),
2084 // but they are always zero-length for us, so we don't write anything
2085 // (the length of the rgbExtra field is inferred from the header_size).
2086 // MENU => rgbExtra: [cbHeaderSize]u8
2087 // MENUEX => rgbExtra: [cbHeaderSize-4]u8
2088
2089 if (resource == .menuex) {
2090 if (node.help_id) |help_id_node| {
2091 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
2092 try data_writer.writeInt(u32, help_id.value, .little);
2093 } else {
2094 try data_writer.writeInt(u32, 0, .little);
2095 }
2096 }
2097
2098 for (node.items, 0..) |item, i| {
2099 const is_last = i == node.items.len - 1;
2100 try self.writeMenuItem(item, data_writer, is_last);
2101 }
2102 }
2103
2104 pub fn writeMenuItem(self: *Compiler, node: *Node, writer: anytype, is_last_of_parent: bool) !void {
2105 switch (node.id) {
2106 .menu_item_separator => {
2107 // This is the 'alternate compability form' of the separator, see
2108 // https://devblogs.microsoft.com/oldnewthing/20080710-00/?p=21673
2109 //
2110 // The 'correct' way is to set the MF_SEPARATOR flag, but the Win32 RC
2111 // compiler still uses this alternate form, so that's what we use too.
2112 var flags = res.MenuItemFlags{};
2113 if (is_last_of_parent) flags.markLast();
2114 try writer.writeInt(u16, flags.value, .little);
2115 try writer.writeInt(u16, 0, .little); // id
2116 try writer.writeInt(u16, 0, .little); // null-terminated UTF-16 text
2117 },
2118 .menu_item => {
2119 const menu_item = @fieldParentPtr(Node.MenuItem, "base", node);
2120 var flags = res.MenuItemFlags{};
2121 for (menu_item.option_list) |option_token| {
2122 // This failing would be a bug in the parser
2123 const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
2124 flags.apply(option);
2125 }
2126 if (is_last_of_parent) flags.markLast();
2127 try writer.writeInt(u16, flags.value, .little);
2128
2129 var result = evaluateNumberExpression(menu_item.result, self.source, self.input_code_pages);
2130 try writer.writeInt(u16, result.asWord(), .little);
2131
2132 var text = try self.parseQuotedStringAsWideString(menu_item.text);
2133 defer self.allocator.free(text);
2134 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2135 },
2136 .popup => {
2137 const popup = @fieldParentPtr(Node.Popup, "base", node);
2138 var flags = res.MenuItemFlags{ .value = res.MF.POPUP };
2139 for (popup.option_list) |option_token| {
2140 // This failing would be a bug in the parser
2141 const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
2142 flags.apply(option);
2143 }
2144 if (is_last_of_parent) flags.markLast();
2145 try writer.writeInt(u16, flags.value, .little);
2146
2147 var text = try self.parseQuotedStringAsWideString(popup.text);
2148 defer self.allocator.free(text);
2149 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2150
2151 for (popup.items, 0..) |item, i| {
2152 const is_last = i == popup.items.len - 1;
2153 try self.writeMenuItem(item, writer, is_last);
2154 }
2155 },
2156 inline .menu_item_ex, .popup_ex => |node_type| {
2157 const menu_item = @fieldParentPtr(node_type.Type(), "base", node);
2158
2159 if (menu_item.type) |flags| {
2160 const value = evaluateNumberExpression(flags, self.source, self.input_code_pages);
2161 try writer.writeInt(u32, value.value, .little);
2162 } else {
2163 try writer.writeInt(u32, 0, .little);
2164 }
2165
2166 if (menu_item.state) |state| {
2167 const value = evaluateNumberExpression(state, self.source, self.input_code_pages);
2168 try writer.writeInt(u32, value.value, .little);
2169 } else {
2170 try writer.writeInt(u32, 0, .little);
2171 }
2172
2173 if (menu_item.id) |id| {
2174 const value = evaluateNumberExpression(id, self.source, self.input_code_pages);
2175 try writer.writeInt(u32, value.value, .little);
2176 } else {
2177 try writer.writeInt(u32, 0, .little);
2178 }
2179
2180 var flags: u16 = 0;
2181 if (is_last_of_parent) flags |= comptime @as(u16, @intCast(res.MF.END));
2182 // This constant doesn't seem to have a named #define, it's different than MF_POPUP
2183 if (node_type == .popup_ex) flags |= 0x01;
2184 try writer.writeInt(u16, flags, .little);
2185
2186 var text = try self.parseQuotedStringAsWideString(menu_item.text);
2187 defer self.allocator.free(text);
2188 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
2189
2190 // Only the combination of the flags u16 and the text bytes can cause
2191 // non-DWORD alignment, so we can just use the byte length of those
2192 // two values to realign to DWORD alignment.
2193 const relevant_bytes = 2 + (text.len + 1) * 2;
2194 try writeDataPadding(writer, @intCast(relevant_bytes));
2195
2196 if (node_type == .popup_ex) {
2197 if (menu_item.help_id) |help_id_node| {
2198 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
2199 try writer.writeInt(u32, help_id.value, .little);
2200 } else {
2201 try writer.writeInt(u32, 0, .little);
2202 }
2203
2204 for (menu_item.items, 0..) |item, i| {
2205 const is_last = i == menu_item.items.len - 1;
2206 try self.writeMenuItem(item, writer, is_last);
2207 }
2208 }
2209 },
2210 else => unreachable,
2211 }
2212 }
2213
2214 pub fn writeVersionInfo(self: *Compiler, node: *Node.VersionInfo, writer: anytype) !void {
2215 var data_buffer = std.ArrayList(u8).init(self.allocator);
2216 defer data_buffer.deinit();
2217 // The node's length field (which is inclusive of the length of all of its children) is a u16
2218 // so limit the node's data size so that we know we can always specify the real size.
2219 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u16));
2220 const data_writer = limited_writer.writer();
2221
2222 try data_writer.writeInt(u16, 0, .little); // placeholder size
2223 try data_writer.writeInt(u16, res.FixedFileInfo.byte_len, .little);
2224 try data_writer.writeInt(u16, res.VersionNode.type_binary, .little);
2225 const key_bytes = std.mem.sliceAsBytes(res.FixedFileInfo.key[0 .. res.FixedFileInfo.key.len + 1]);
2226 try data_writer.writeAll(key_bytes);
2227 // The number of bytes written up to this point is always the same, since the name
2228 // of the node is a constant (FixedFileInfo.key). The total number of bytes
2229 // written so far is 38, so we need 2 padding bytes to get back to DWORD alignment
2230 try data_writer.writeInt(u16, 0, .little);
2231
2232 var fixed_file_info = res.FixedFileInfo{};
2233 for (node.fixed_info) |fixed_info| {
2234 switch (fixed_info.id) {
2235 .version_statement => {
2236 const version_statement = @fieldParentPtr(Node.VersionStatement, "base", fixed_info);
2237 const version_type = rc.VersionInfo.map.get(version_statement.type.slice(self.source)).?;
2238
2239 // Ensure that all parts are cleared for each version, to properly account for
2240 // potential duplicate PRODUCTVERSION/FILEVERSION statements
2241 switch (version_type) {
2242 .file_version => @memset(&fixed_file_info.file_version.parts, 0),
2243 .product_version => @memset(&fixed_file_info.product_version.parts, 0),
2244 else => unreachable,
2245 }
2246
2247 for (version_statement.parts, 0..) |part, i| {
2248 const part_value = evaluateNumberExpression(part, self.source, self.input_code_pages);
2249 if (part_value.is_long) {
2250 try self.addErrorDetails(.{
2251 .err = .rc_would_error_u16_with_l_suffix,
2252 .type = .warning,
2253 .token = part.getFirstToken(),
2254 .token_span_end = part.getLastToken(),
2255 .extra = .{ .statement_with_u16_param = switch (version_type) {
2256 .file_version => .fileversion,
2257 .product_version => .productversion,
2258 else => unreachable,
2259 } },
2260 });
2261 try self.addErrorDetails(.{
2262 .err = .rc_would_error_u16_with_l_suffix,
2263 .print_source_line = false,
2264 .type = .note,
2265 .token = part.getFirstToken(),
2266 .token_span_end = part.getLastToken(),
2267 .extra = .{ .statement_with_u16_param = switch (version_type) {
2268 .file_version => .fileversion,
2269 .product_version => .productversion,
2270 else => unreachable,
2271 } },
2272 });
2273 }
2274 switch (version_type) {
2275 .file_version => {
2276 fixed_file_info.file_version.parts[i] = part_value.asWord();
2277 },
2278 .product_version => {
2279 fixed_file_info.product_version.parts[i] = part_value.asWord();
2280 },
2281 else => unreachable,
2282 }
2283 }
2284 },
2285 .simple_statement => {
2286 const statement = @fieldParentPtr(Node.SimpleStatement, "base", fixed_info);
2287 const statement_type = rc.VersionInfo.map.get(statement.identifier.slice(self.source)).?;
2288 const value = evaluateNumberExpression(statement.value, self.source, self.input_code_pages);
2289 switch (statement_type) {
2290 .file_flags_mask => fixed_file_info.file_flags_mask = value.value,
2291 .file_flags => fixed_file_info.file_flags = value.value,
2292 .file_os => fixed_file_info.file_os = value.value,
2293 .file_type => fixed_file_info.file_type = value.value,
2294 .file_subtype => fixed_file_info.file_subtype = value.value,
2295 else => unreachable,
2296 }
2297 },
2298 else => unreachable,
2299 }
2300 }
2301 try fixed_file_info.write(data_writer);
2302
2303 for (node.block_statements) |statement| {
2304 self.writeVersionNode(statement, data_writer, &data_buffer) catch |err| switch (err) {
2305 error.NoSpaceLeft => {
2306 try self.addErrorDetails(.{
2307 .err = .version_node_size_exceeds_max,
2308 .token = node.id,
2309 });
2310 return self.addErrorDetailsAndFail(.{
2311 .err = .version_node_size_exceeds_max,
2312 .type = .note,
2313 .token = statement.getFirstToken(),
2314 .token_span_end = statement.getLastToken(),
2315 });
2316 },
2317 else => |e| return e,
2318 };
2319 }
2320
2321 // We know that data_buffer.items.len is within the limits of a u16, since we
2322 // limited the writer to maxInt(u16)
2323 const data_size: u16 = @intCast(data_buffer.items.len);
2324 // And now that we know the full size of this node (including its children), set its size
2325 std.mem.writeInt(u16, data_buffer.items[0..2], data_size, .little);
2326
2327 var header = try self.resourceHeader(node.id, node.versioninfo, .{
2328 .data_size = data_size,
2329 });
2330 defer header.deinit(self.allocator);
2331
2332 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2333
2334 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
2335
2336 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2337 try writeResourceData(writer, data_fbs.reader(), data_size);
2338 }
2339
2340 /// Expects writer to be a LimitedWriter limited to u16, meaning all writes to
2341 /// the writer within this function could return error.NoSpaceLeft, and that buf.items.len
2342 /// will never be able to exceed maxInt(u16).
2343 pub fn writeVersionNode(self: *Compiler, node: *Node, writer: anytype, buf: *std.ArrayList(u8)) !void {
2344 // We can assume that buf.items.len will never be able to exceed the limits of a u16
2345 try writeDataPadding(writer, @as(u16, @intCast(buf.items.len)));
2346
2347 const node_and_children_size_offset = buf.items.len;
2348 try writer.writeInt(u16, 0, .little); // placeholder for size
2349 const data_size_offset = buf.items.len;
2350 try writer.writeInt(u16, 0, .little); // placeholder for data size
2351 const data_type_offset = buf.items.len;
2352 // Data type is string unless the node contains values that are numbers.
2353 try writer.writeInt(u16, res.VersionNode.type_string, .little);
2354
2355 switch (node.id) {
2356 inline .block, .block_value => |node_type| {
2357 const block_or_value = @fieldParentPtr(node_type.Type(), "base", node);
2358 const parsed_key = try self.parseQuotedStringAsWideString(block_or_value.key);
2359 defer self.allocator.free(parsed_key);
2360
2361 const parsed_key_to_first_null = std.mem.sliceTo(parsed_key, 0);
2362 try writer.writeAll(std.mem.sliceAsBytes(parsed_key_to_first_null[0 .. parsed_key_to_first_null.len + 1]));
2363
2364 var has_number_value: bool = false;
2365 for (block_or_value.values) |value_value_node_uncasted| {
2366 const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
2367 if (value_value_node.expression.isNumberExpression()) {
2368 has_number_value = true;
2369 break;
2370 }
2371 }
2372 // The units used here are dependent on the type. If there are any numbers, then
2373 // this is a byte count. If there are only strings, then this is a count of
2374 // UTF-16 code units.
2375 //
2376 // The Win32 RC compiler miscompiles this count in the case of values that
2377 // have a mix of numbers and strings. This is detected and a warning is emitted
2378 // during parsing, so we can just do the correct thing here.
2379 var values_size: usize = 0;
2380
2381 try writeDataPadding(writer, @intCast(buf.items.len));
2382
2383 for (block_or_value.values, 0..) |value_value_node_uncasted, i| {
2384 const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
2385 const value_node = value_value_node.expression;
2386 if (value_node.isNumberExpression()) {
2387 const number = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
2388 // This is used to write u16 or u32 depending on the number's suffix
2389 const data_wrapper = Data{ .number = number };
2390 try data_wrapper.write(writer);
2391 // Numbers use byte count
2392 values_size += if (number.is_long) 4 else 2;
2393 } else {
2394 std.debug.assert(value_node.isStringLiteral());
2395 const literal_node = value_node.cast(.literal).?;
2396 const parsed_value = try self.parseQuotedStringAsWideString(literal_node.token);
2397 defer self.allocator.free(parsed_value);
2398
2399 const parsed_to_first_null = std.mem.sliceTo(parsed_value, 0);
2400 try writer.writeAll(std.mem.sliceAsBytes(parsed_to_first_null));
2401 // Strings use UTF-16 code-unit count including the null-terminator, but
2402 // only if there are no number values in the list.
2403 var value_size = parsed_to_first_null.len;
2404 if (has_number_value) value_size *= 2; // 2 bytes per UTF-16 code unit
2405 values_size += value_size;
2406 // The null-terminator is only included if there's a trailing comma
2407 // or this is the last value. If the value evaluates to empty, then
2408 // it never gets a null terminator. If there was an explicit null-terminator
2409 // in the string, we still need to potentially add one since we already
2410 // sliced to the terminator.
2411 const is_last = i == block_or_value.values.len - 1;
2412 const is_empty = parsed_to_first_null.len == 0;
2413 const is_only = block_or_value.values.len == 1;
2414 if ((!is_empty or !is_only) and (is_last or value_value_node.trailing_comma)) {
2415 try writer.writeInt(u16, 0, .little);
2416 values_size += if (has_number_value) 2 else 1;
2417 }
2418 }
2419 }
2420 var data_size_slice = buf.items[data_size_offset..];
2421 std.mem.writeInt(u16, data_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(values_size)), .little);
2422
2423 if (has_number_value) {
2424 const data_type_slice = buf.items[data_type_offset..];
2425 std.mem.writeInt(u16, data_type_slice[0..@sizeOf(u16)], res.VersionNode.type_binary, .little);
2426 }
2427
2428 if (node_type == .block) {
2429 const block = block_or_value;
2430 for (block.children) |child| {
2431 try self.writeVersionNode(child, writer, buf);
2432 }
2433 }
2434 },
2435 else => unreachable,
2436 }
2437
2438 const node_and_children_size = buf.items.len - node_and_children_size_offset;
2439 const node_and_children_size_slice = buf.items[node_and_children_size_offset..];
2440 std.mem.writeInt(u16, node_and_children_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(node_and_children_size)), .little);
2441 }
2442
2443 pub fn writeStringTable(self: *Compiler, node: *Node.StringTable) !void {
2444 const language = getLanguageFromOptionalStatements(node.optional_statements, self.source, self.input_code_pages) orelse self.state.language;
2445
2446 for (node.strings) |string_node| {
2447 const string = @fieldParentPtr(Node.StringTableString, "base", string_node);
2448 const string_id_data = try self.evaluateDataExpression(string.id);
2449 const string_id = string_id_data.number.asWord();
2450
2451 self.state.string_tables.set(
2452 self.arena,
2453 language,
2454 string_id,
2455 string.string,
2456 &node.base,
2457 self.source,
2458 self.input_code_pages,
2459 self.state.version,
2460 self.state.characteristics,
2461 ) catch |err| switch (err) {
2462 error.StringAlreadyDefined => {
2463 // It might be nice to have these errors point to the ids rather than the
2464 // string tokens, but that would mean storing the id token of each string
2465 // which doesn't seem worth it just for slightly better error messages.
2466 try self.addErrorDetails(ErrorDetails{
2467 .err = .string_already_defined,
2468 .token = string.string,
2469 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
2470 });
2471 const existing_def_table = self.state.string_tables.tables.getPtr(language).?;
2472 const existing_definition = existing_def_table.get(string_id).?;
2473 return self.addErrorDetailsAndFail(ErrorDetails{
2474 .err = .string_already_defined,
2475 .type = .note,
2476 .token = existing_definition,
2477 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
2478 });
2479 },
2480 error.OutOfMemory => |e| return e,
2481 };
2482 }
2483 }
2484
2485 /// Expects this to be a top-level LANGUAGE statement
2486 pub fn writeLanguageStatement(self: *Compiler, node: *Node.LanguageStatement) void {
2487 const primary = Compiler.evaluateNumberExpression(node.primary_language_id, self.source, self.input_code_pages);
2488 const sublanguage = Compiler.evaluateNumberExpression(node.sublanguage_id, self.source, self.input_code_pages);
2489 self.state.language.primary_language_id = @truncate(primary.value);
2490 self.state.language.sublanguage_id = @truncate(sublanguage.value);
2491 }
2492
2493 /// Expects this to be a top-level VERSION or CHARACTERISTICS statement
2494 pub fn writeTopLevelSimpleStatement(self: *Compiler, node: *Node.SimpleStatement) void {
2495 const value = Compiler.evaluateNumberExpression(node.value, self.source, self.input_code_pages);
2496 const statement_type = rc.TopLevelKeywords.map.get(node.identifier.slice(self.source)).?;
2497 switch (statement_type) {
2498 .characteristics => self.state.characteristics = value.value,
2499 .version => self.state.version = value.value,
2500 else => unreachable,
2501 }
2502 }
2503
2504 pub const ResourceHeaderOptions = struct {
2505 language: ?res.Language = null,
2506 data_size: DWORD = 0,
2507 };
2508
2509 pub fn resourceHeader(self: *Compiler, id_token: Token, type_token: Token, options: ResourceHeaderOptions) !ResourceHeader {
2510 const id_bytes = self.sourceBytesForToken(id_token);
2511 const type_bytes = self.sourceBytesForToken(type_token);
2512 return ResourceHeader.init(
2513 self.allocator,
2514 id_bytes,
2515 type_bytes,
2516 options.data_size,
2517 options.language orelse self.state.language,
2518 self.state.version,
2519 self.state.characteristics,
2520 ) catch |err| switch (err) {
2521 error.OutOfMemory => |e| return e,
2522 error.TypeNonAsciiOrdinal => {
2523 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes).?;
2524 try self.addErrorDetails(.{
2525 .err = .invalid_digit_character_in_ordinal,
2526 .type = .err,
2527 .token = type_token,
2528 });
2529 return self.addErrorDetailsAndFail(.{
2530 .err = .win32_non_ascii_ordinal,
2531 .type = .note,
2532 .token = type_token,
2533 .print_source_line = false,
2534 .extra = .{ .number = win32_rc_ordinal.ordinal },
2535 });
2536 },
2537 error.IdNonAsciiOrdinal => {
2538 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes).?;
2539 try self.addErrorDetails(.{
2540 .err = .invalid_digit_character_in_ordinal,
2541 .type = .err,
2542 .token = id_token,
2543 });
2544 return self.addErrorDetailsAndFail(.{
2545 .err = .win32_non_ascii_ordinal,
2546 .type = .note,
2547 .token = id_token,
2548 .print_source_line = false,
2549 .extra = .{ .number = win32_rc_ordinal.ordinal },
2550 });
2551 },
2552 };
2553 }
2554
2555 pub const ResourceHeader = struct {
2556 name_value: NameOrOrdinal,
2557 type_value: NameOrOrdinal,
2558 language: res.Language,
2559 memory_flags: MemoryFlags,
2560 data_size: DWORD,
2561 version: DWORD,
2562 characteristics: DWORD,
2563 data_version: DWORD = 0,
2564
2565 pub const InitError = error{ OutOfMemory, IdNonAsciiOrdinal, TypeNonAsciiOrdinal };
2566
2567 pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {
2568 const type_value = type: {
2569 const resource_type = Resource.fromString(type_bytes);
2570 if (res.RT.fromResource(resource_type)) |rt_constant| {
2571 break :type NameOrOrdinal{ .ordinal = @intFromEnum(rt_constant) };
2572 } else {
2573 break :type try NameOrOrdinal.fromString(allocator, type_bytes);
2574 }
2575 };
2576 errdefer type_value.deinit(allocator);
2577 if (type_value == .name) {
2578 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes)) |_| {
2579 return error.TypeNonAsciiOrdinal;
2580 }
2581 }
2582
2583 const name_value = try NameOrOrdinal.fromString(allocator, id_bytes);
2584 errdefer name_value.deinit(allocator);
2585 if (name_value == .name) {
2586 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes)) |_| {
2587 return error.IdNonAsciiOrdinal;
2588 }
2589 }
2590
2591 const predefined_resource_type = type_value.predefinedResourceType();
2592
2593 return ResourceHeader{
2594 .name_value = name_value,
2595 .type_value = type_value,
2596 .data_size = data_size,
2597 .memory_flags = MemoryFlags.defaults(predefined_resource_type),
2598 .language = language,
2599 .version = version,
2600 .characteristics = characteristics,
2601 };
2602 }
2603
2604 pub fn deinit(self: ResourceHeader, allocator: Allocator) void {
2605 self.name_value.deinit(allocator);
2606 self.type_value.deinit(allocator);
2607 }
2608
2609 pub const SizeInfo = struct {
2610 bytes: u32,
2611 padding_after_name: u2,
2612 };
2613
2614 fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {
2615 var header_size: u32 = 8;
2616 header_size = try std.math.add(
2617 u32,
2618 header_size,
2619 std.math.cast(u32, self.name_value.byteLen()) orelse return error.Overflow,
2620 );
2621 header_size = try std.math.add(
2622 u32,
2623 header_size,
2624 std.math.cast(u32, self.type_value.byteLen()) orelse return error.Overflow,
2625 );
2626 const padding_after_name = numPaddingBytesNeeded(header_size);
2627 header_size = try std.math.add(u32, header_size, padding_after_name);
2628 header_size = try std.math.add(u32, header_size, 16);
2629 return .{ .bytes = header_size, .padding_after_name = padding_after_name };
2630 }
2631
2632 pub fn writeAssertNoOverflow(self: ResourceHeader, writer: anytype) !void {
2633 return self.writeSizeInfo(writer, self.calcSize() catch unreachable);
2634 }
2635
2636 pub fn write(self: ResourceHeader, writer: anytype, err_ctx: errors.DiagnosticsContext) !void {
2637 const size_info = self.calcSize() catch {
2638 try err_ctx.diagnostics.append(.{
2639 .err = .resource_data_size_exceeds_max,
2640 .token = err_ctx.token,
2641 });
2642 return error.CompileError;
2643 };
2644 return self.writeSizeInfo(writer, size_info);
2645 }
2646
2647 fn writeSizeInfo(self: ResourceHeader, writer: anytype, size_info: SizeInfo) !void {
2648 try writer.writeInt(DWORD, self.data_size, .little); // DataSize
2649 try writer.writeInt(DWORD, size_info.bytes, .little); // HeaderSize
2650 try self.type_value.write(writer); // TYPE
2651 try self.name_value.write(writer); // NAME
2652 try writer.writeByteNTimes(0, size_info.padding_after_name);
2653
2654 try writer.writeInt(DWORD, self.data_version, .little); // DataVersion
2655 try writer.writeInt(WORD, self.memory_flags.value, .little); // MemoryFlags
2656 try writer.writeInt(WORD, self.language.asInt(), .little); // LanguageId
2657 try writer.writeInt(DWORD, self.version, .little); // Version
2658 try writer.writeInt(DWORD, self.characteristics, .little); // Characteristics
2659 }
2660
2661 pub fn predefinedResourceType(self: ResourceHeader) ?res.RT {
2662 return self.type_value.predefinedResourceType();
2663 }
2664
2665 pub fn applyMemoryFlags(self: *ResourceHeader, tokens: []Token, source: []const u8) void {
2666 applyToMemoryFlags(&self.memory_flags, tokens, source);
2667 }
2668
2669 pub fn applyOptionalStatements(self: *ResourceHeader, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
2670 applyToOptionalStatements(&self.language, &self.version, &self.characteristics, statements, source, code_page_lookup);
2671 }
2672 };
2673
2674 fn applyToMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
2675 for (tokens) |token| {
2676 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2677 flags.set(attribute);
2678 }
2679 }
2680
2681 /// RT_GROUP_ICON and RT_GROUP_CURSOR have their own special rules for memory flags
2682 fn applyToGroupMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
2683 // There's probably a cleaner implementation of this, but this will result in the same
2684 // flags as the Win32 RC compiler for all 986,410 K-permutations of memory flags
2685 // for an ICON resource.
2686 //
2687 // This was arrived at by iterating over the permutations and creating a
2688 // list where each line looks something like this:
2689 // MOVEABLE PRELOAD -> 0x1050 (MOVEABLE|PRELOAD|DISCARDABLE)
2690 //
2691 // and then noticing a few things:
2692
2693 // 1. Any permutation that does not have PRELOAD in it just uses the
2694 // default flags.
2695 const initial_flags = flags.*;
2696 var flags_set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
2697 for (tokens) |token| {
2698 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2699 flags_set.insert(attribute);
2700 }
2701 if (!flags_set.contains(.preload)) return;
2702
2703 // 2. Any permutation of flags where applying only the PRELOAD and LOADONCALL flags
2704 // results in no actual change by the end will just use the default flags.
2705 // For example, `PRELOAD LOADONCALL` will result in default flags, but
2706 // `LOADONCALL PRELOAD` will have PRELOAD set after they are both applied in order.
2707 for (tokens) |token| {
2708 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2709 switch (attribute) {
2710 .preload, .loadoncall => flags.set(attribute),
2711 else => {},
2712 }
2713 }
2714 if (flags.value == initial_flags.value) return;
2715
2716 // 3. If none of DISCARDABLE, SHARED, or PURE is specified, then PRELOAD
2717 // implies `flags &= ~SHARED` and LOADONCALL implies `flags |= SHARED`
2718 const shared_set = comptime blk: {
2719 var set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
2720 set.insert(.discardable);
2721 set.insert(.shared);
2722 set.insert(.pure);
2723 break :blk set;
2724 };
2725 const discardable_shared_or_pure_specified = flags_set.intersectWith(shared_set).count() != 0;
2726 for (tokens) |token| {
2727 const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
2728 flags.setGroup(attribute, !discardable_shared_or_pure_specified);
2729 }
2730 }
2731
2732 /// Only handles the 'base' optional statements that are shared between resource types.
2733 fn applyToOptionalStatements(language: *res.Language, version: *u32, characteristics: *u32, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
2734 for (statements) |node| switch (node.id) {
2735 .language_statement => {
2736 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
2737 language.* = languageFromLanguageStatement(language_statement, source, code_page_lookup);
2738 },
2739 .simple_statement => {
2740 const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", node);
2741 const statement_type = rc.OptionalStatements.map.get(simple_statement.identifier.slice(source)) orelse continue;
2742 const result = Compiler.evaluateNumberExpression(simple_statement.value, source, code_page_lookup);
2743 switch (statement_type) {
2744 .version => version.* = result.value,
2745 .characteristics => characteristics.* = result.value,
2746 else => unreachable, // only VERSION and CHARACTERISTICS should be in an optional statements list
2747 }
2748 },
2749 else => {},
2750 };
2751 }
2752
2753 pub fn languageFromLanguageStatement(language_statement: *const Node.LanguageStatement, source: []const u8, code_page_lookup: *const CodePageLookup) res.Language {
2754 const primary = Compiler.evaluateNumberExpression(language_statement.primary_language_id, source, code_page_lookup);
2755 const sublanguage = Compiler.evaluateNumberExpression(language_statement.sublanguage_id, source, code_page_lookup);
2756 return .{
2757 .primary_language_id = @truncate(primary.value),
2758 .sublanguage_id = @truncate(sublanguage.value),
2759 };
2760 }
2761
2762 pub fn getLanguageFromOptionalStatements(statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) ?res.Language {
2763 for (statements) |node| switch (node.id) {
2764 .language_statement => {
2765 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
2766 return languageFromLanguageStatement(language_statement, source, code_page_lookup);
2767 },
2768 else => continue,
2769 };
2770 return null;
2771 }
2772
2773 pub fn writeEmptyResource(writer: anytype) !void {
2774 const header = ResourceHeader{
2775 .name_value = .{ .ordinal = 0 },
2776 .type_value = .{ .ordinal = 0 },
2777 .language = .{
2778 .primary_language_id = 0,
2779 .sublanguage_id = 0,
2780 },
2781 .memory_flags = .{ .value = 0 },
2782 .data_size = 0,
2783 .version = 0,
2784 .characteristics = 0,
2785 };
2786 try header.writeAssertNoOverflow(writer);
2787 }
2788
2789 pub fn sourceBytesForToken(self: *Compiler, token: Token) SourceBytes {
2790 return .{
2791 .slice = token.slice(self.source),
2792 .code_page = self.input_code_pages.getForToken(token),
2793 };
2794 }
2795
2796 /// Helper that calls parseQuotedStringAsWideString with the relevant context
2797 /// Resulting slice is allocated by `self.allocator`.
2798 pub fn parseQuotedStringAsWideString(self: *Compiler, token: Token) ![:0]u16 {
2799 return literals.parseQuotedStringAsWideString(
2800 self.allocator,
2801 self.sourceBytesForToken(token),
2802 .{
2803 .start_column = token.calculateColumn(self.source, 8, null),
2804 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
2805 },
2806 );
2807 }
2808
2809 /// Helper that calls parseQuotedStringAsAsciiString with the relevant context
2810 /// Resulting slice is allocated by `self.allocator`.
2811 pub fn parseQuotedStringAsAsciiString(self: *Compiler, token: Token) ![]u8 {
2812 return literals.parseQuotedStringAsAsciiString(
2813 self.allocator,
2814 self.sourceBytesForToken(token),
2815 .{
2816 .start_column = token.calculateColumn(self.source, 8, null),
2817 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
2818 },
2819 );
2820 }
2821
2822 fn addErrorDetails(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
2823 try self.diagnostics.append(details);
2824 }
2825
2826 fn addErrorDetailsAndFail(self: *Compiler, details: ErrorDetails) error{ CompileError, OutOfMemory } {
2827 try self.addErrorDetails(details);
2828 return error.CompileError;
2829 }
2830};
2831
2832pub const OpenSearchPathError = std.fs.Dir.OpenError;
2833
2834fn openSearchPathDir(dir: std.fs.Dir, path: []const u8) OpenSearchPathError!std.fs.Dir {
2835 // Validate the search path to avoid possible unreachable on invalid paths,
2836 // see https://github.com/ziglang/zig/issues/15607 for why this is currently necessary.
2837 try validateSearchPath(path);
2838 return dir.openDir(path, .{});
2839}
2840
2841/// Very crude attempt at validating a path. This is imperfect
2842/// and AFAIK it is effectively impossible to implement perfect path
2843/// validation, since it ultimately depends on the underlying filesystem.
2844/// Note that this function won't be necessary if/when
2845/// https://github.com/ziglang/zig/issues/15607
2846/// is accepted/implemented.
2847fn validateSearchPath(path: []const u8) error{BadPathName}!void {
2848 switch (builtin.os.tag) {
2849 .windows => {
2850 // This will return error.BadPathName on non-Win32 namespaced paths
2851 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
2852 // Those path types are something of an unavoidable way to
2853 // still hit unreachable during the openDir call.
2854 var component_iterator = try std.fs.path.componentIterator(path);
2855 while (component_iterator.next()) |component| {
2856 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2857 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
2858 }
2859 },
2860 else => {
2861 if (std.mem.indexOfScalar(u8, path, 0) != null) return error.BadPathName;
2862 },
2863 }
2864}
2865
2866pub const SearchDir = struct {
2867 dir: std.fs.Dir,
2868 path: ?[]const u8,
2869
2870 pub fn deinit(self: *SearchDir, allocator: Allocator) void {
2871 self.dir.close();
2872 if (self.path) |path| {
2873 allocator.free(path);
2874 }
2875 }
2876};
2877
2878/// Slurps the first `size` bytes read into `slurped_header`
2879pub fn HeaderSlurpingReader(comptime size: usize, comptime ReaderType: anytype) type {
2880 return struct {
2881 child_reader: ReaderType,
2882 bytes_read: usize = 0,
2883 slurped_header: [size]u8 = [_]u8{0x00} ** size,
2884
2885 pub const Error = ReaderType.Error;
2886 pub const Reader = std.io.Reader(*@This(), Error, read);
2887
2888 pub fn read(self: *@This(), buf: []u8) Error!usize {
2889 const amt = try self.child_reader.read(buf);
2890 if (self.bytes_read < size) {
2891 const bytes_to_add = @min(amt, size - self.bytes_read);
2892 const end_index = self.bytes_read + bytes_to_add;
2893 @memcpy(self.slurped_header[self.bytes_read..end_index], buf[0..bytes_to_add]);
2894 }
2895 self.bytes_read +|= amt;
2896 return amt;
2897 }
2898
2899 pub fn reader(self: *@This()) Reader {
2900 return .{ .context = self };
2901 }
2902 };
2903}
2904
2905pub fn headerSlurpingReader(comptime size: usize, reader: anytype) HeaderSlurpingReader(size, @TypeOf(reader)) {
2906 return .{ .child_reader = reader };
2907}
2908
2909/// Sort of like std.io.LimitedReader, but a Writer.
2910/// Returns an error if writing the requested number of bytes
2911/// would ever exceed bytes_left, i.e. it does not always
2912/// write up to the limit and instead will error if the
2913/// limit would be breached if the entire slice was written.
2914pub fn LimitedWriter(comptime WriterType: type) type {
2915 return struct {
2916 inner_writer: WriterType,
2917 bytes_left: u64,
2918
2919 pub const Error = error{NoSpaceLeft} || WriterType.Error;
2920 pub const Writer = std.io.Writer(*Self, Error, write);
2921
2922 const Self = @This();
2923
2924 pub fn write(self: *Self, bytes: []const u8) Error!usize {
2925 if (bytes.len > self.bytes_left) return error.NoSpaceLeft;
2926 const amt = try self.inner_writer.write(bytes);
2927 self.bytes_left -= amt;
2928 return amt;
2929 }
2930
2931 pub fn writer(self: *Self) Writer {
2932 return .{ .context = self };
2933 }
2934 };
2935}
2936
2937/// Returns an initialised `LimitedWriter`
2938/// `bytes_left` is a `u64` to be able to take 64 bit file offsets
2939pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) {
2940 return .{ .inner_writer = inner_writer, .bytes_left = bytes_left };
2941}
2942
2943test "limitedWriter basic usage" {
2944 var buf: [4]u8 = undefined;
2945 var fbs = std.io.fixedBufferStream(&buf);
2946 var limited_stream = limitedWriter(fbs.writer(), 4);
2947 var writer = limited_stream.writer();
2948
2949 try std.testing.expectEqual(@as(usize, 3), try writer.write("123"));
2950 try std.testing.expectEqualSlices(u8, "123", buf[0..3]);
2951 try std.testing.expectError(error.NoSpaceLeft, writer.write("45"));
2952 try std.testing.expectEqual(@as(usize, 1), try writer.write("4"));
2953 try std.testing.expectEqualSlices(u8, "1234", buf[0..4]);
2954 try std.testing.expectError(error.NoSpaceLeft, writer.write("5"));
2955}
2956
2957pub const FontDir = struct {
2958 fonts: std.ArrayListUnmanaged(Font) = .{},
2959 /// To keep track of which ids are set and where they were set from
2960 ids: std.AutoHashMapUnmanaged(u16, Token) = .{},
2961
2962 pub const Font = struct {
2963 id: u16,
2964 header_bytes: [148]u8,
2965 };
2966
2967 pub fn deinit(self: *FontDir, allocator: Allocator) void {
2968 self.fonts.deinit(allocator);
2969 }
2970
2971 pub fn add(self: *FontDir, allocator: Allocator, font: Font, id_token: Token) !void {
2972 try self.ids.putNoClobber(allocator, font.id, id_token);
2973 try self.fonts.append(allocator, font);
2974 }
2975
2976 pub fn writeResData(self: *FontDir, compiler: *Compiler, writer: anytype) !void {
2977 if (self.fonts.items.len == 0) return;
2978
2979 // We know the number of fonts is limited to maxInt(u16) because fonts
2980 // must have a valid and unique u16 ordinal ID (trying to specify a FONT
2981 // with e.g. id 65537 will wrap around to 1 and be ignored if there's already
2982 // a font with that ID in the file).
2983 const num_fonts: u16 = @intCast(self.fonts.items.len);
2984
2985 // u16 count + [(u16 id + 150 bytes) for each font]
2986 // Note: This works out to a maximum data_size of 9,961,322.
2987 const data_size: u32 = 2 + (2 + 150) * num_fonts;
2988
2989 var header = Compiler.ResourceHeader{
2990 .name_value = try NameOrOrdinal.nameFromString(compiler.allocator, .{ .slice = "FONTDIR", .code_page = .windows1252 }),
2991 .type_value = NameOrOrdinal{ .ordinal = @intFromEnum(res.RT.FONTDIR) },
2992 .memory_flags = res.MemoryFlags.defaults(res.RT.FONTDIR),
2993 .language = compiler.state.language,
2994 .version = compiler.state.version,
2995 .characteristics = compiler.state.characteristics,
2996 .data_size = data_size,
2997 };
2998 defer header.deinit(compiler.allocator);
2999
3000 try header.writeAssertNoOverflow(writer);
3001 try writer.writeInt(u16, num_fonts, .little);
3002 for (self.fonts.items) |font| {
3003 // The format of the FONTDIR is a strange beast.
3004 // Technically, each FONT is seemingly meant to be written as a
3005 // FONTDIRENTRY with two trailing NUL-terminated strings corresponding to
3006 // the 'device name' and 'face name' of the .FNT file, but:
3007 //
3008 // 1. When dealing with .FNT files, the Win32 implementation
3009 // gets the device name and face name from the wrong locations,
3010 // so it's basically never going to write the real device/face name
3011 // strings.
3012 // 2. When dealing with files 76-140 bytes long, the Win32 implementation
3013 // can just crash (if there are no NUL bytes in the file).
3014 // 3. The 32-bit Win32 rc.exe uses a 148 byte size for the portion of
3015 // the FONTDIRENTRY before the NUL-terminated strings, which
3016 // does not match the documented FONTDIRENTRY size that (presumably)
3017 // this format is meant to be using, so anything iterating the
3018 // FONTDIR according to the available documentation will get bogus results.
3019 // 4. The FONT resource can be used for non-.FNT types like TTF and OTF,
3020 // in which case emulating the Win32 behavior of unconditionally
3021 // interpreting the bytes as a .FNT and trying to grab device/face names
3022 // from random bytes in the TTF/OTF file can lead to weird behavior
3023 // and errors in the Win32 implementation (for example, the device/face
3024 // name fields are offsets into the file where the NUL-terminated
3025 // string is located, but the Win32 implementation actually treats
3026 // them as signed so if they are negative then the Win32 implementation
3027 // will error; this happening for TTF fonts would just be a bug
3028 // since the TTF could otherwise be valid)
3029 // 5. The FONTDIR resource doesn't actually seem to be used at all by
3030 // anything that I've found, and instead in Windows 3.0 and newer
3031 // it seems like the FONT resources are always just iterated/accessed
3032 // directly without ever looking at the FONTDIR.
3033 //
3034 // All of these combined means that we:
3035 // - Do not need or want to emulate Win32 behavior here
3036 // - For maximum simplicity and compatibility, we just write the first
3037 // 148 bytes of the file without any interpretation (padded with
3038 // zeroes to get up to 148 bytes if necessary), and then
3039 // unconditionally write two NUL bytes, meaning that we always
3040 // write 'device name' and 'face name' as if they were 0-length
3041 // strings.
3042 //
3043 // This gives us byte-for-byte .RES compatibility in the common case while
3044 // allowing us to avoid any erroneous errors caused by trying to read
3045 // the face/device name from a bogus location. Note that the Win32
3046 // implementation never actually writes the real device/face name here
3047 // anyway (except in the bizarre case that a .FNT file has the proper
3048 // device/face name offsets within a reserved section of the .FNT file)
3049 // so there's no feasible way that anything can actually think that the
3050 // device name/face name in the FONTDIR is reliable.
3051
3052 // First, the ID is written, though
3053 try writer.writeInt(u16, font.id, .little);
3054 try writer.writeAll(&font.header_bytes);
3055 try writer.writeByteNTimes(0, 2);
3056 }
3057 try Compiler.writeDataPadding(writer, data_size);
3058 }
3059};
3060
3061pub const StringTablesByLanguage = struct {
3062 /// String tables for each language are written to the .res file in order depending on
3063 /// when the first STRINGTABLE for the language was defined, and all blocks for a given
3064 /// language are written contiguously.
3065 /// Using an ArrayHashMap here gives us this property for free.
3066 tables: std.AutoArrayHashMapUnmanaged(res.Language, StringTable) = .{},
3067
3068 pub fn deinit(self: *StringTablesByLanguage, allocator: Allocator) void {
3069 self.tables.deinit(allocator);
3070 }
3071
3072 pub fn set(
3073 self: *StringTablesByLanguage,
3074 allocator: Allocator,
3075 language: res.Language,
3076 id: u16,
3077 string_token: Token,
3078 node: *Node,
3079 source: []const u8,
3080 code_page_lookup: *const CodePageLookup,
3081 version: u32,
3082 characteristics: u32,
3083 ) StringTable.SetError!void {
3084 var get_or_put_result = try self.tables.getOrPut(allocator, language);
3085 if (!get_or_put_result.found_existing) {
3086 get_or_put_result.value_ptr.* = StringTable{};
3087 }
3088 return get_or_put_result.value_ptr.set(allocator, id, string_token, node, source, code_page_lookup, version, characteristics);
3089 }
3090};
3091
3092pub const StringTable = struct {
3093 /// Blocks are written to the .res file in order depending on when the first string
3094 /// was added to the block (i.e. `STRINGTABLE { 16 "b" 0 "a" }` would then get written
3095 /// with block ID 2 (the one with "b") first and block ID 1 (the one with "a") second).
3096 /// Using an ArrayHashMap here gives us this property for free.
3097 blocks: std.AutoArrayHashMapUnmanaged(u16, Block) = .{},
3098
3099 pub const Block = struct {
3100 strings: std.ArrayListUnmanaged(Token) = .{},
3101 set_indexes: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 },
3102 memory_flags: MemoryFlags = MemoryFlags.defaults(res.RT.STRING),
3103 characteristics: u32,
3104 version: u32,
3105
3106 /// Returns the index to insert the string into the `strings` list.
3107 /// Returns null if the string should be appended.
3108 fn getInsertionIndex(self: *Block, index: u8) ?u8 {
3109 std.debug.assert(!self.set_indexes.isSet(index));
3110
3111 const first_set = self.set_indexes.findFirstSet() orelse return null;
3112 if (first_set > index) return 0;
3113
3114 const last_set = 15 - @clz(self.set_indexes.mask);
3115 if (index > last_set) return null;
3116
3117 var bit = first_set + 1;
3118 var insertion_index: u8 = 1;
3119 while (bit != index) : (bit += 1) {
3120 if (self.set_indexes.isSet(bit)) insertion_index += 1;
3121 }
3122 return insertion_index;
3123 }
3124
3125 fn getTokenIndex(self: *Block, string_index: u8) ?u8 {
3126 const count = self.strings.items.len;
3127 if (count == 0) return null;
3128 if (count == 1) return 0;
3129
3130 const first_set = self.set_indexes.findFirstSet() orelse unreachable;
3131 if (first_set == string_index) return 0;
3132 const last_set = 15 - @clz(self.set_indexes.mask);
3133 if (last_set == string_index) return @intCast(count - 1);
3134
3135 if (first_set == last_set) return null;
3136
3137 var bit = first_set + 1;
3138 var token_index: u8 = 1;
3139 while (bit < last_set) : (bit += 1) {
3140 if (!self.set_indexes.isSet(bit)) continue;
3141 if (bit == string_index) return token_index;
3142 token_index += 1;
3143 }
3144 return null;
3145 }
3146
3147 fn dump(self: *Block) void {
3148 var bit_it = self.set_indexes.iterator(.{});
3149 var string_index: usize = 0;
3150 while (bit_it.next()) |bit_index| {
3151 const token = self.strings.items[string_index];
3152 std.debug.print("{}: [{}] {any}\n", .{ bit_index, string_index, token });
3153 string_index += 1;
3154 }
3155 }
3156
3157 pub fn applyAttributes(self: *Block, string_table: *Node.StringTable, source: []const u8, code_page_lookup: *const CodePageLookup) void {
3158 Compiler.applyToMemoryFlags(&self.memory_flags, string_table.common_resource_attributes, source);
3159 var dummy_language: res.Language = undefined;
3160 Compiler.applyToOptionalStatements(&dummy_language, &self.version, &self.characteristics, string_table.optional_statements, source, code_page_lookup);
3161 }
3162
3163 fn trimToDoubleNUL(comptime T: type, str: []const T) []const T {
3164 var last_was_null = false;
3165 for (str, 0..) |c, i| {
3166 if (c == 0) {
3167 if (last_was_null) return str[0 .. i - 1];
3168 last_was_null = true;
3169 } else {
3170 last_was_null = false;
3171 }
3172 }
3173 return str;
3174 }
3175
3176 test "trimToDoubleNUL" {
3177 try std.testing.expectEqualStrings("a\x00b", trimToDoubleNUL(u8, "a\x00b"));
3178 try std.testing.expectEqualStrings("a", trimToDoubleNUL(u8, "a\x00\x00b"));
3179 }
3180
3181 pub fn writeResData(self: *Block, compiler: *Compiler, language: res.Language, block_id: u16, writer: anytype) !void {
3182 var data_buffer = std.ArrayList(u8).init(compiler.allocator);
3183 defer data_buffer.deinit();
3184 const data_writer = data_buffer.writer();
3185
3186 var i: u8 = 0;
3187 var string_i: u8 = 0;
3188 while (true) : (i += 1) {
3189 if (!self.set_indexes.isSet(i)) {
3190 try data_writer.writeInt(u16, 0, .little);
3191 if (i == 15) break else continue;
3192 }
3193
3194 const string_token = self.strings.items[string_i];
3195 const slice = string_token.slice(compiler.source);
3196 const column = string_token.calculateColumn(compiler.source, 8, null);
3197 const code_page = compiler.input_code_pages.getForToken(string_token);
3198 const bytes = SourceBytes{ .slice = slice, .code_page = code_page };
3199 const utf16_string = try literals.parseQuotedStringAsWideString(compiler.allocator, bytes, .{
3200 .start_column = column,
3201 .diagnostics = .{ .diagnostics = compiler.diagnostics, .token = string_token },
3202 });
3203 defer compiler.allocator.free(utf16_string);
3204
3205 const trimmed_string = trim: {
3206 // Two NUL characters in a row act as a terminator
3207 // Note: This is only the case for STRINGTABLE strings
3208 const trimmed = trimToDoubleNUL(u16, utf16_string);
3209 // We also want to trim any trailing NUL characters
3210 break :trim std.mem.trimRight(u16, trimmed, &[_]u16{0});
3211 };
3212
3213 // String literals are limited to maxInt(u15) codepoints, so these UTF-16 encoded
3214 // strings are limited to maxInt(u15) * 2 = 65,534 code units (since 2 is the
3215 // maximum number of UTF-16 code units per codepoint).
3216 // This leaves room for exactly one NUL terminator.
3217 var string_len_in_utf16_code_units: u16 = @intCast(trimmed_string.len);
3218 // If the option is set, then a NUL terminator is added unconditionally.
3219 // We already trimmed any trailing NULs, so we know it will be a new addition to the string.
3220 if (compiler.null_terminate_string_table_strings) string_len_in_utf16_code_units += 1;
3221 try data_writer.writeInt(u16, string_len_in_utf16_code_units, .little);
3222 try data_writer.writeAll(std.mem.sliceAsBytes(trimmed_string));
3223 if (compiler.null_terminate_string_table_strings) {
3224 try data_writer.writeInt(u16, 0, .little);
3225 }
3226
3227 if (i == 15) break;
3228 string_i += 1;
3229 }
3230
3231 // This intCast will never be able to fail due to the length constraints on string literals.
3232 //
3233 // - STRINGTABLE resource definitions can can only provide one string literal per index.
3234 // - STRINGTABLE strings are limited to maxInt(u16) UTF-16 code units (see 'string_len_in_utf16_code_units'
3235 // above), which means that the maximum number of bytes per string literal is
3236 // 2 * maxInt(u16) = 131,070 (since there are 2 bytes per UTF-16 code unit).
3237 // - Each Block/RT_STRING resource includes exactly 16 strings and each have a 2 byte
3238 // length field, so the maximum number of total bytes in a RT_STRING resource's data is
3239 // 16 * (131,070 + 2) = 2,097,152 which is well within the u32 max.
3240 //
3241 // Note: The string literal maximum length is enforced by the lexer.
3242 const data_size: u32 = @intCast(data_buffer.items.len);
3243
3244 const header = Compiler.ResourceHeader{
3245 .name_value = .{ .ordinal = block_id },
3246 .type_value = .{ .ordinal = @intFromEnum(res.RT.STRING) },
3247 .memory_flags = self.memory_flags,
3248 .language = language,
3249 .version = self.version,
3250 .characteristics = self.characteristics,
3251 .data_size = data_size,
3252 };
3253 // The only variable parts of the header are name and type, which in this case
3254 // we fully control and know are numbers, so they have a fixed size.
3255 try header.writeAssertNoOverflow(writer);
3256
3257 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
3258 try Compiler.writeResourceData(writer, data_fbs.reader(), data_size);
3259 }
3260 };
3261
3262 pub fn deinit(self: *StringTable, allocator: Allocator) void {
3263 var it = self.blocks.iterator();
3264 while (it.next()) |entry| {
3265 entry.value_ptr.strings.deinit(allocator);
3266 }
3267 self.blocks.deinit(allocator);
3268 }
3269
3270 const SetError = error{StringAlreadyDefined} || Allocator.Error;
3271
3272 pub fn set(
3273 self: *StringTable,
3274 allocator: Allocator,
3275 id: u16,
3276 string_token: Token,
3277 node: *Node,
3278 source: []const u8,
3279 code_page_lookup: *const CodePageLookup,
3280 version: u32,
3281 characteristics: u32,
3282 ) SetError!void {
3283 const block_id = (id / 16) + 1;
3284 const string_index: u8 = @intCast(id & 0xF);
3285
3286 var get_or_put_result = try self.blocks.getOrPut(allocator, block_id);
3287 if (!get_or_put_result.found_existing) {
3288 get_or_put_result.value_ptr.* = Block{ .version = version, .characteristics = characteristics };
3289 get_or_put_result.value_ptr.applyAttributes(node.cast(.string_table).?, source, code_page_lookup);
3290 } else {
3291 if (get_or_put_result.value_ptr.set_indexes.isSet(string_index)) {
3292 return error.StringAlreadyDefined;
3293 }
3294 }
3295
3296 var block = get_or_put_result.value_ptr;
3297 if (block.getInsertionIndex(string_index)) |insertion_index| {
3298 try block.strings.insert(allocator, insertion_index, string_token);
3299 } else {
3300 try block.strings.append(allocator, string_token);
3301 }
3302 block.set_indexes.set(string_index);
3303 }
3304
3305 pub fn get(self: *StringTable, id: u16) ?Token {
3306 const block_id = (id / 16) + 1;
3307 const string_index: u8 = @intCast(id & 0xF);
3308
3309 const block = self.blocks.getPtr(block_id) orelse return null;
3310 const token_index = block.getTokenIndex(string_index) orelse return null;
3311 return block.strings.items[token_index];
3312 }
3313
3314 pub fn dump(self: *StringTable) !void {
3315 var it = self.iterator();
3316 while (it.next()) |entry| {
3317 std.debug.print("block: {}\n", .{entry.key_ptr.*});
3318 entry.value_ptr.dump();
3319 }
3320 }
3321};
3322
3323test "StringTable" {
3324 const S = struct {
3325 fn makeDummyToken(id: usize) Token {
3326 return Token{
3327 .id = .invalid,
3328 .start = id,
3329 .end = id,
3330 .line_number = id,
3331 };
3332 }
3333 };
3334 const allocator = std.testing.allocator;
3335 var string_table = StringTable{};
3336 defer string_table.deinit(allocator);
3337
3338 var code_page_lookup = CodePageLookup.init(allocator, .windows1252);
3339 defer code_page_lookup.deinit();
3340
3341 var dummy_node = Node.StringTable{
3342 .type = S.makeDummyToken(0),
3343 .common_resource_attributes = &.{},
3344 .optional_statements = &.{},
3345 .begin_token = S.makeDummyToken(0),
3346 .strings = &.{},
3347 .end_token = S.makeDummyToken(0),
3348 };
3349
3350 // randomize an array of ids 0-99
3351 var ids = ids: {
3352 var buf: [100]u16 = undefined;
3353 var i: u16 = 0;
3354 while (i < buf.len) : (i += 1) {
3355 buf[i] = i;
3356 }
3357 break :ids buf;
3358 };
3359 var prng = std.Random.DefaultPrng.init(0);
3360 var random = prng.random();
3361 random.shuffle(u16, &ids);
3362
3363 // set each one in the randomized order
3364 for (ids) |id| {
3365 try string_table.set(allocator, id, S.makeDummyToken(id), &dummy_node.base, "", &code_page_lookup, 0, 0);
3366 }
3367
3368 // make sure each one exists and is the right value when gotten
3369 var id: u16 = 0;
3370 while (id < 100) : (id += 1) {
3371 const dummy = S.makeDummyToken(id);
3372 try std.testing.expectError(error.StringAlreadyDefined, string_table.set(allocator, id, dummy, &dummy_node.base, "", &code_page_lookup, 0, 0));
3373 try std.testing.expectEqual(dummy, string_table.get(id).?);
3374 }
3375
3376 // make sure non-existent string ids are not found
3377 try std.testing.expectEqual(@as(?Token, null), string_table.get(100));
3378}
src/resinator/errors.zig deleted-1060
...@@ -1,1060 +0,0 @@
1const std = @import("std");
2const Token = @import("lex.zig").Token;
3const SourceMappings = @import("source_mapping.zig").SourceMappings;
4const utils = @import("utils.zig");
5const rc = @import("rc.zig");
6const res = @import("res.zig");
7const ico = @import("ico.zig");
8const bmp = @import("bmp.zig");
9const parse = @import("parse.zig");
10const lang = @import("lang.zig");
11const CodePage = @import("code_pages.zig").CodePage;
12const builtin = @import("builtin");
13const native_endian = builtin.cpu.arch.endian();
14
15pub const Diagnostics = struct {
16 errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
17 /// Append-only, cannot handle removing strings.
18 /// Expects to own all strings within the list.
19 strings: std.ArrayListUnmanaged([]const u8) = .{},
20 allocator: std.mem.Allocator,
21
22 pub fn init(allocator: std.mem.Allocator) Diagnostics {
23 return .{
24 .allocator = allocator,
25 };
26 }
27
28 pub fn deinit(self: *Diagnostics) void {
29 self.errors.deinit(self.allocator);
30 for (self.strings.items) |str| {
31 self.allocator.free(str);
32 }
33 self.strings.deinit(self.allocator);
34 }
35
36 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
37 try self.errors.append(self.allocator, error_details);
38 }
39
40 const SmallestStringIndexType = std.meta.Int(.unsigned, @min(
41 @bitSizeOf(ErrorDetails.FileOpenError.FilenameStringIndex),
42 @min(
43 @bitSizeOf(ErrorDetails.IconReadError.FilenameStringIndex),
44 @bitSizeOf(ErrorDetails.BitmapReadError.FilenameStringIndex),
45 ),
46 ));
47
48 /// Returns the index of the added string as the SmallestStringIndexType
49 /// in order to avoid needing to `@intCast` it at callsites of putString.
50 /// Instead, this function will error if the index would ever exceed the
51 /// smallest FilenameStringIndex of an ErrorDetails type.
52 pub fn putString(self: *Diagnostics, str: []const u8) !SmallestStringIndexType {
53 if (self.strings.items.len >= std.math.maxInt(SmallestStringIndexType)) {
54 return error.OutOfMemory; // ran out of string indexes
55 }
56 const dupe = try self.allocator.dupe(u8, str);
57 const index = self.strings.items.len;
58 try self.strings.append(self.allocator, dupe);
59 return @intCast(index);
60 }
61
62 pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.io.tty.Config, source_mappings: ?SourceMappings) void {
63 std.debug.getStderrMutex().lock();
64 defer std.debug.getStderrMutex().unlock();
65 const stderr = std.io.getStdErr().writer();
66 for (self.errors.items) |err_details| {
67 renderErrorMessage(self.allocator, stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return;
68 }
69 }
70
71 pub fn renderToStdErrDetectTTY(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void {
72 const tty_config = std.io.tty.detectConfig(std.io.getStdErr());
73 return self.renderToStdErr(cwd, source, tty_config, source_mappings);
74 }
75
76 pub fn contains(self: *const Diagnostics, err: ErrorDetails.Error) bool {
77 for (self.errors.items) |details| {
78 if (details.err == err) return true;
79 }
80 return false;
81 }
82
83 pub fn containsAny(self: *const Diagnostics, errors: []const ErrorDetails.Error) bool {
84 for (self.errors.items) |details| {
85 for (errors) |err| {
86 if (details.err == err) return true;
87 }
88 }
89 return false;
90 }
91};
92
93/// Contains enough context to append errors/warnings/notes etc
94pub const DiagnosticsContext = struct {
95 diagnostics: *Diagnostics,
96 token: Token,
97};
98
99pub const ErrorDetails = struct {
100 err: Error,
101 token: Token,
102 /// If non-null, should be before `token`. If null, `token` is assumed to be the start.
103 token_span_start: ?Token = null,
104 /// If non-null, should be after `token`. If null, `token` is assumed to be the end.
105 token_span_end: ?Token = null,
106 type: Type = .err,
107 print_source_line: bool = true,
108 extra: union {
109 none: void,
110 expected: Token.Id,
111 number: u32,
112 expected_types: ExpectedTypes,
113 resource: rc.Resource,
114 string_and_language: StringAndLanguage,
115 file_open_error: FileOpenError,
116 icon_read_error: IconReadError,
117 icon_dir: IconDirContext,
118 bmp_read_error: BitmapReadError,
119 accelerator_error: AcceleratorError,
120 statement_with_u16_param: StatementWithU16Param,
121 menu_or_class: enum { class, menu },
122 } = .{ .none = {} },
123
124 pub const Type = enum {
125 /// Fatal error, stops compilation
126 err,
127 /// Warning that does not affect compilation result
128 warning,
129 /// A note that typically provides further context for a warning/error
130 note,
131 /// An invisible diagnostic that is not printed to stderr but can
132 /// provide information useful when comparing the behavior of different
133 /// implementations. For example, a hint is emitted when a FONTDIR resource
134 /// was included in the .RES file which is significant because rc.exe
135 /// does something different than us, but ultimately it's not important
136 /// enough to be a warning/note.
137 hint,
138 };
139
140 comptime {
141 // all fields in the extra union should be 32 bits or less
142 for (std.meta.fields(std.meta.fieldInfo(ErrorDetails, .extra).type)) |field| {
143 std.debug.assert(@bitSizeOf(field.type) <= 32);
144 }
145 }
146
147 pub const StatementWithU16Param = enum(u32) {
148 fileversion,
149 productversion,
150 language,
151 };
152
153 pub const StringAndLanguage = packed struct(u32) {
154 id: u16,
155 language: res.Language,
156 };
157
158 pub const FileOpenError = packed struct(u32) {
159 err: FileOpenErrorEnum,
160 filename_string_index: FilenameStringIndex,
161
162 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum));
163 pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError);
164
165 pub fn enumFromError(err: std.fs.File.OpenError) FileOpenErrorEnum {
166 return switch (err) {
167 inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)),
168 };
169 }
170 };
171
172 pub const IconReadError = packed struct(u32) {
173 err: IconReadErrorEnum,
174 icon_type: enum(u1) { cursor, icon },
175 filename_string_index: FilenameStringIndex,
176
177 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(IconReadErrorEnum) - 1);
178 pub const IconReadErrorEnum = std.meta.FieldEnum(ico.ReadError);
179
180 pub fn enumFromError(err: ico.ReadError) IconReadErrorEnum {
181 return switch (err) {
182 inline else => |e| @field(ErrorDetails.IconReadError.IconReadErrorEnum, @errorName(e)),
183 };
184 }
185 };
186
187 pub const IconDirContext = packed struct(u32) {
188 icon_type: enum(u1) { cursor, icon },
189 icon_format: ico.ImageFormat,
190 index: u16,
191 bitmap_version: ico.BitmapHeader.Version = .unknown,
192 _: Padding = 0,
193
194 pub const Padding = std.meta.Int(.unsigned, 15 - @bitSizeOf(ico.BitmapHeader.Version) - @bitSizeOf(ico.ImageFormat));
195 };
196
197 pub const BitmapReadError = packed struct(u32) {
198 err: BitmapReadErrorEnum,
199 filename_string_index: FilenameStringIndex,
200
201 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(BitmapReadErrorEnum));
202 pub const BitmapReadErrorEnum = std.meta.FieldEnum(bmp.ReadError);
203
204 pub fn enumFromError(err: bmp.ReadError) BitmapReadErrorEnum {
205 return switch (err) {
206 inline else => |e| @field(ErrorDetails.BitmapReadError.BitmapReadErrorEnum, @errorName(e)),
207 };
208 }
209 };
210
211 pub const BitmapUnsupportedDIB = packed struct(u32) {
212 dib_version: ico.BitmapHeader.Version,
213 filename_string_index: FilenameStringIndex,
214
215 pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(ico.BitmapHeader.Version));
216 };
217
218 pub const AcceleratorError = packed struct(u32) {
219 err: AcceleratorErrorEnum,
220 _: Padding = 0,
221
222 pub const Padding = std.meta.Int(.unsigned, 32 - @bitSizeOf(AcceleratorErrorEnum));
223 pub const AcceleratorErrorEnum = std.meta.FieldEnum(res.ParseAcceleratorKeyStringError);
224
225 pub fn enumFromError(err: res.ParseAcceleratorKeyStringError) AcceleratorErrorEnum {
226 return switch (err) {
227 inline else => |e| @field(ErrorDetails.AcceleratorError.AcceleratorErrorEnum, @errorName(e)),
228 };
229 }
230 };
231
232 pub const ExpectedTypes = packed struct(u32) {
233 number: bool = false,
234 number_expression: bool = false,
235 string_literal: bool = false,
236 accelerator_type_or_option: bool = false,
237 control_class: bool = false,
238 literal: bool = false,
239 // Note: This being 0 instead of undefined is arbitrary and something of a workaround,
240 // see https://github.com/ziglang/zig/issues/15395
241 _: u26 = 0,
242
243 pub const strings = std.ComptimeStringMap([]const u8, .{
244 .{ "number", "number" },
245 .{ "number_expression", "number expression" },
246 .{ "string_literal", "quoted string literal" },
247 .{ "accelerator_type_or_option", "accelerator type or option [ASCII, VIRTKEY, etc]" },
248 .{ "control_class", "control class [BUTTON, EDIT, etc]" },
249 .{ "literal", "unquoted literal" },
250 });
251
252 pub fn writeCommaSeparated(self: ExpectedTypes, writer: anytype) !void {
253 const struct_info = @typeInfo(ExpectedTypes).Struct;
254 const num_real_fields = struct_info.fields.len - 1;
255 const num_padding_bits = @bitSizeOf(ExpectedTypes) - num_real_fields;
256 const mask = std.math.maxInt(struct_info.backing_integer.?) >> num_padding_bits;
257 const relevant_bits_only = @as(struct_info.backing_integer.?, @bitCast(self)) & mask;
258 const num_set_bits = @popCount(relevant_bits_only);
259
260 var i: usize = 0;
261 inline for (struct_info.fields) |field_info| {
262 if (field_info.type != bool) continue;
263 if (i == num_set_bits) return;
264 if (@field(self, field_info.name)) {
265 try writer.writeAll(strings.get(field_info.name).?);
266 i += 1;
267 if (num_set_bits > 2 and i != num_set_bits) {
268 try writer.writeAll(", ");
269 } else if (i != num_set_bits) {
270 try writer.writeByte(' ');
271 }
272 if (num_set_bits > 1 and i == num_set_bits - 1) {
273 try writer.writeAll("or ");
274 }
275 }
276 }
277 }
278 };
279
280 pub const Error = enum {
281 // Lexer
282 unfinished_string_literal,
283 string_literal_too_long,
284 invalid_number_with_exponent,
285 invalid_digit_character_in_number_literal,
286 illegal_byte,
287 illegal_byte_outside_string_literals,
288 illegal_codepoint_outside_string_literals,
289 illegal_byte_order_mark,
290 illegal_private_use_character,
291 found_c_style_escaped_quote,
292 code_page_pragma_missing_left_paren,
293 code_page_pragma_missing_right_paren,
294 code_page_pragma_invalid_code_page,
295 code_page_pragma_not_integer,
296 code_page_pragma_overflow,
297 code_page_pragma_unsupported_code_page,
298
299 // Parser
300 unfinished_raw_data_block,
301 unfinished_string_table_block,
302 /// `expected` is populated.
303 expected_token,
304 /// `expected_types` is populated
305 expected_something_else,
306 /// `resource` is populated
307 resource_type_cant_use_raw_data,
308 /// `resource` is populated
309 id_must_be_ordinal,
310 /// `resource` is populated
311 name_or_id_not_allowed,
312 string_resource_as_numeric_type,
313 ascii_character_not_equivalent_to_virtual_key_code,
314 empty_menu_not_allowed,
315 rc_would_miscompile_version_value_padding,
316 rc_would_miscompile_version_value_byte_count,
317 code_page_pragma_in_included_file,
318 nested_resource_level_exceeds_max,
319 too_many_dialog_controls,
320 nested_expression_level_exceeds_max,
321 close_paren_expression,
322 unary_plus_expression,
323 rc_could_miscompile_control_params,
324
325 // Compiler
326 /// `string_and_language` is populated
327 string_already_defined,
328 font_id_already_defined,
329 /// `file_open_error` is populated
330 file_open_error,
331 /// `accelerator_error` is populated
332 invalid_accelerator_key,
333 accelerator_type_required,
334 rc_would_miscompile_control_padding,
335 rc_would_miscompile_control_class_ordinal,
336 /// `icon_dir` is populated
337 rc_would_error_on_icon_dir,
338 /// `icon_dir` is populated
339 format_not_supported_in_icon_dir,
340 /// `resource` is populated and contains the expected type
341 icon_dir_and_resource_type_mismatch,
342 /// `icon_read_error` is populated
343 icon_read_error,
344 /// `icon_dir` is populated
345 rc_would_error_on_bitmap_version,
346 /// `icon_dir` is populated
347 max_icon_ids_exhausted,
348 /// `bmp_read_error` is populated
349 bmp_read_error,
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 ignored bytes.
352 bmp_ignored_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 missing bytes.
355 bmp_missing_palette_bytes,
356 /// `number` is populated and contains a string index for which the string contains
357 /// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.
358 rc_would_miscompile_bmp_palette_padding,
359 /// `number` is populated and contains a string index for which the string contains
360 /// the bytes of two `u64`s (native endian). The first contains the number of missing
361 /// palette bytes and the second contains the max number of missing palette bytes.
362 /// If type is `.note`, then `extra` is `none`.
363 bmp_too_many_missing_palette_bytes,
364 resource_header_size_exceeds_max,
365 resource_data_size_exceeds_max,
366 control_extra_data_size_exceeds_max,
367 version_node_size_exceeds_max,
368 fontdir_size_exceeds_max,
369 /// `number` is populated and contains a string index for the filename
370 number_expression_as_filename,
371 /// `number` is populated and contains the control ID that is a duplicate
372 control_id_already_defined,
373 /// `number` is populated and contains the disallowed codepoint
374 invalid_filename,
375 /// `statement_with_u16_param` is populated
376 rc_would_error_u16_with_l_suffix,
377 result_contains_fontdir,
378 /// `number` is populated and contains the ordinal value that the id would be miscompiled to
379 rc_would_miscompile_dialog_menu_id,
380 /// `number` is populated and contains the ordinal value that the value would be miscompiled to
381 rc_would_miscompile_dialog_class,
382 /// `menu_or_class` is populated and contains the type of the parameter statement
383 rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
384 rc_would_miscompile_dialog_menu_id_starts_with_digit,
385 dialog_menu_id_was_uppercased,
386 /// `menu_or_class` is populated and contains the type of the parameter statement
387 duplicate_menu_or_class_skipped,
388 invalid_digit_character_in_ordinal,
389
390 // Literals
391 /// `number` is populated
392 rc_would_miscompile_codepoint_byte_swap,
393 /// `number` is populated
394 rc_would_miscompile_codepoint_skip,
395 tab_converted_to_spaces,
396
397 // General (used in various places)
398 /// `number` is populated and contains the value that the ordinal would have in the Win32 RC compiler implementation
399 win32_non_ascii_ordinal,
400
401 // Initialization
402 /// `file_open_error` is populated, but `filename_string_index` is not
403 failed_to_open_cwd,
404 };
405
406 pub fn render(self: ErrorDetails, writer: anytype, source: []const u8, strings: []const []const u8) !void {
407 switch (self.err) {
408 .unfinished_string_literal => {
409 return writer.print("unfinished string literal at '{s}', expected closing '\"'", .{self.token.nameForErrorDisplay(source)});
410 },
411 .string_literal_too_long => {
412 return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number});
413 },
414 .invalid_number_with_exponent => {
415 return writer.print("base 10 number literal with exponent is not allowed: {s}", .{self.token.slice(source)});
416 },
417 .invalid_digit_character_in_number_literal => switch (self.type) {
418 .err, .warning => return writer.writeAll("non-ASCII digit characters are not allowed in number literals"),
419 .note => return writer.writeAll("the Win32 RC compiler allows non-ASCII digit characters, but will miscompile them"),
420 .hint => return,
421 },
422 .illegal_byte => {
423 return writer.print("character '{s}' is not allowed", .{std.fmt.fmtSliceEscapeUpper(self.token.slice(source))});
424 },
425 .illegal_byte_outside_string_literals => {
426 return writer.print("character '{s}' is not allowed outside of string literals", .{std.fmt.fmtSliceEscapeUpper(self.token.slice(source))});
427 },
428 .illegal_codepoint_outside_string_literals => {
429 // This is somewhat hacky, but we know that:
430 // - This error is only possible with codepoints outside of the Windows-1252 character range
431 // - So, the only supported code page that could generate this error is UTF-8
432 // Therefore, we just assume the token bytes are UTF-8 and decode them to get the illegal
433 // codepoint.
434 //
435 // FIXME: Support other code pages if they become relevant
436 const bytes = self.token.slice(source);
437 const codepoint = std.unicode.utf8Decode(bytes) catch unreachable;
438 return writer.print("codepoint <U+{X:0>4}> is not allowed outside of string literals", .{codepoint});
439 },
440 .illegal_byte_order_mark => {
441 return writer.writeAll("byte order mark <U+FEFF> is not allowed");
442 },
443 .illegal_private_use_character => {
444 return writer.writeAll("private use character <U+E000> is not allowed");
445 },
446 .found_c_style_escaped_quote => {
447 return writer.writeAll("escaping quotes with \\\" is not allowed (use \"\" instead)");
448 },
449 .code_page_pragma_missing_left_paren => {
450 return writer.writeAll("expected left parenthesis after 'code_page' in #pragma code_page");
451 },
452 .code_page_pragma_missing_right_paren => {
453 return writer.writeAll("expected right parenthesis after '<number>' in #pragma code_page");
454 },
455 .code_page_pragma_invalid_code_page => {
456 return writer.writeAll("invalid or unknown code page in #pragma code_page");
457 },
458 .code_page_pragma_not_integer => {
459 return writer.writeAll("code page is not a valid integer in #pragma code_page");
460 },
461 .code_page_pragma_overflow => {
462 return writer.writeAll("code page too large in #pragma code_page");
463 },
464 .code_page_pragma_unsupported_code_page => {
465 // We know that the token slice is a well-formed #pragma code_page(N), so
466 // we can skip to the first ( and then get the number that follows
467 const token_slice = self.token.slice(source);
468 var number_start = std.mem.indexOfScalar(u8, token_slice, '(').? + 1;
469 while (std.ascii.isWhitespace(token_slice[number_start])) {
470 number_start += 1;
471 }
472 var number_slice = token_slice[number_start..number_start];
473 while (std.ascii.isDigit(token_slice[number_start + number_slice.len])) {
474 number_slice.len += 1;
475 }
476 const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable;
477 const code_page = CodePage.getByIdentifier(number) catch unreachable;
478 // TODO: Improve or maybe add a note making it more clear that the code page
479 // is valid and that the code page is unsupported purely due to a limitation
480 // in this compiler.
481 return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number });
482 },
483 .unfinished_raw_data_block => {
484 return writer.print("unfinished raw data block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});
485 },
486 .unfinished_string_table_block => {
487 return writer.print("unfinished STRINGTABLE block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});
488 },
489 .expected_token => {
490 return writer.print("expected '{s}', got '{s}'", .{ self.extra.expected.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });
491 },
492 .expected_something_else => {
493 try writer.writeAll("expected ");
494 try self.extra.expected_types.writeCommaSeparated(writer);
495 return writer.print("; got '{s}'", .{self.token.nameForErrorDisplay(source)});
496 },
497 .resource_type_cant_use_raw_data => switch (self.type) {
498 .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() }),
499 .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)}),
500 .hint => return,
501 },
502 .id_must_be_ordinal => {
503 try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{s}'", .{ self.extra.resource.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });
504 },
505 .name_or_id_not_allowed => {
506 try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()});
507 },
508 .string_resource_as_numeric_type => switch (self.type) {
509 .err, .warning => try writer.writeAll("the number 6 (RT_STRING) cannot be used as a resource type"),
510 .note => try writer.writeAll("using RT_STRING directly likely results in an invalid .res file, use a STRINGTABLE instead"),
511 .hint => return,
512 },
513 .ascii_character_not_equivalent_to_virtual_key_code => {
514 // TODO: Better wording? This is what the Win32 RC compiler emits.
515 // This occurs when VIRTKEY and a control code is specified ("^c", etc)
516 try writer.writeAll("ASCII character not equivalent to virtual key code");
517 },
518 .empty_menu_not_allowed => {
519 try writer.print("empty menu of type '{s}' not allowed", .{self.token.nameForErrorDisplay(source)});
520 },
521 .rc_would_miscompile_version_value_padding => switch (self.type) {
522 .err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}),
523 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma between the key and the quoted string", .{}),
524 .hint => return,
525 },
526 .rc_would_miscompile_version_value_byte_count => switch (self.type) {
527 .err, .warning => return writer.print("the byte count of this value would be miscompiled by the Win32 RC compiler", .{}),
528 .note => return writer.print("to avoid the potential miscompilation, do not mix numbers and strings within a value", .{}),
529 .hint => return,
530 },
531 .code_page_pragma_in_included_file => {
532 try writer.print("#pragma code_page is not supported in an included resource file", .{});
533 },
534 .nested_resource_level_exceeds_max => switch (self.type) {
535 .err, .warning => {
536 const max = switch (self.extra.resource) {
537 .versioninfo => parse.max_nested_version_level,
538 .menu, .menuex => parse.max_nested_menu_level,
539 else => unreachable,
540 };
541 return writer.print("{s} contains too many nested children (max is {})", .{ self.extra.resource.nameForErrorDisplay(), max });
542 },
543 .note => return writer.print("max {s} nesting level exceeded here", .{self.extra.resource.nameForErrorDisplay()}),
544 .hint => return,
545 },
546 .too_many_dialog_controls => switch (self.type) {
547 .err, .warning => return writer.print("{s} contains too many controls (max is {})", .{ self.extra.resource.nameForErrorDisplay(), std.math.maxInt(u16) }),
548 .note => return writer.writeAll("maximum number of controls exceeded here"),
549 .hint => return,
550 },
551 .nested_expression_level_exceeds_max => switch (self.type) {
552 .err, .warning => return writer.print("expression contains too many syntax levels (max is {})", .{parse.max_nested_expression_level}),
553 .note => return writer.print("maximum expression level exceeded here", .{}),
554 .hint => return,
555 },
556 .close_paren_expression => {
557 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");
558 },
559 .unary_plus_expression => {
560 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 +");
561 },
562 .rc_could_miscompile_control_params => switch (self.type) {
563 .err, .warning => return writer.print("this token could be erroneously skipped over by the Win32 RC compiler", .{}),
564 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}),
565 .hint => return,
566 },
567 .string_already_defined => switch (self.type) {
568 .err, .warning => {
569 const language_id = self.extra.string_and_language.language.asInt();
570 const language_name = language_name: {
571 if (std.meta.intToEnum(lang.LanguageId, language_id)) |lang_enum_val| {
572 break :language_name @tagName(lang_enum_val);
573 } else |_| {}
574 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
575 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
576 }
577 break :language_name "<UNKNOWN>";
578 };
579 return writer.print("string with id {d} (0x{X}) already defined for language {s} (0x{X})", .{ self.extra.string_and_language.id, self.extra.string_and_language.id, language_name, language_id });
580 },
581 .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 }),
582 .hint => return,
583 },
584 .font_id_already_defined => switch (self.type) {
585 .err => return writer.print("font with id {d} already defined", .{self.extra.number}),
586 .warning => return writer.print("skipped duplicate font with id {d}", .{self.extra.number}),
587 .note => return writer.print("previous definition of font with id {d} here", .{self.extra.number}),
588 .hint => return,
589 },
590 .file_open_error => {
591 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) });
592 },
593 .invalid_accelerator_key => {
594 try writer.print("invalid accelerator key '{s}': {s}", .{ self.token.nameForErrorDisplay(source), @tagName(self.extra.accelerator_error.err) });
595 },
596 .accelerator_type_required => {
597 try writer.print("accelerator type [ASCII or VIRTKEY] required when key is an integer", .{});
598 },
599 .rc_would_miscompile_control_padding => switch (self.type) {
600 .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)", .{}),
601 .note => return writer.print("to avoid the potential miscompilation, consider removing any 'control data' blocks from the controls in this dialog", .{}),
602 .hint => return,
603 },
604 .rc_would_miscompile_control_class_ordinal => switch (self.type) {
605 .err, .warning => return writer.print("the control class of this CONTROL would be miscompiled by the Win32 RC compiler", .{}),
606 .note => return writer.print("to avoid the potential miscompilation, consider specifying the control class using a string (BUTTON, EDIT, etc) instead of a number", .{}),
607 .hint => return,
608 },
609 .rc_would_error_on_icon_dir => switch (self.type) {
610 .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) }),
611 .note => {
612 // The only note supported is one specific to exactly this combination
613 if (!(self.extra.icon_dir.icon_type == .icon and self.extra.icon_dir.icon_format == .riff)) unreachable;
614 try writer.print("animated RIFF icons within resource groups may not be well supported, consider using an animated icon file (.ani) instead", .{});
615 },
616 .hint => return,
617 },
618 .format_not_supported_in_icon_dir => {
619 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) });
620 },
621 .icon_dir_and_resource_type_mismatch => {
622 const unexpected_type: rc.Resource = if (self.extra.resource == .icon) .cursor else .icon;
623 // TODO: Better wording
624 try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() });
625 },
626 .icon_read_error => {
627 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) });
628 },
629 .rc_would_error_on_bitmap_version => switch (self.type) {
630 .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}'", .{
631 self.extra.icon_dir.index,
632 @tagName(self.extra.icon_dir.icon_type),
633 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
634 ico.BitmapHeader.Version.@"nt3.1".nameForErrorDisplay(),
635 }),
636 .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", .{
637 self.extra.icon_dir.index,
638 @tagName(self.extra.icon_dir.icon_type),
639 self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
640 }),
641 .note => unreachable,
642 .hint => return,
643 },
644 .max_icon_ids_exhausted => switch (self.type) {
645 .err, .warning => try writer.print("maximum global icon/cursor ids exhausted (max is {})", .{std.math.maxInt(u16) - 1}),
646 .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) }),
647 .hint => return,
648 },
649 .bmp_read_error => {
650 try writer.print("invalid bitmap file '{s}': {s}", .{ strings[self.extra.bmp_read_error.filename_string_index], @tagName(self.extra.bmp_read_error.err) });
651 },
652 .bmp_ignored_palette_bytes => {
653 const bytes = strings[self.extra.number];
654 const ignored_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
655 try writer.print("bitmap has {d} extra bytes preceding the pixel data which will be ignored", .{ignored_bytes});
656 },
657 .bmp_missing_palette_bytes => {
658 const bytes = strings[self.extra.number];
659 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
660 try writer.print("bitmap has {d} missing color palette bytes which will be padded with zeroes", .{missing_bytes});
661 },
662 .rc_would_miscompile_bmp_palette_padding => {
663 const bytes = strings[self.extra.number];
664 const miscompiled_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
665 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});
666 },
667 .bmp_too_many_missing_palette_bytes => switch (self.type) {
668 .err, .warning => {
669 const bytes = strings[self.extra.number];
670 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
671 const max_missing_bytes = std.mem.readInt(u64, bytes[8..16], native_endian);
672 try writer.print("bitmap has {} missing color palette bytes which exceeds the maximum of {}", .{ missing_bytes, max_missing_bytes });
673 },
674 // TODO: command line option
675 .note => try writer.writeAll("the maximum number of missing color palette bytes is configurable via <<TODO command line option>>"),
676 .hint => return,
677 },
678 .resource_header_size_exceeds_max => {
679 try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)});
680 },
681 .resource_data_size_exceeds_max => switch (self.type) {
682 .err, .warning => return writer.print("resource's data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
683 .note => return writer.print("maximum data length exceeded here", .{}),
684 .hint => return,
685 },
686 .control_extra_data_size_exceeds_max => switch (self.type) {
687 .err, .warning => try writer.print("control data length exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
688 .note => return writer.print("maximum control data length exceeded here", .{}),
689 .hint => return,
690 },
691 .version_node_size_exceeds_max => switch (self.type) {
692 .err, .warning => return writer.print("version node tree size exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
693 .note => return writer.print("maximum tree size exceeded while writing this child", .{}),
694 .hint => return,
695 },
696 .fontdir_size_exceeds_max => switch (self.type) {
697 .err, .warning => return writer.print("FONTDIR data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
698 .note => return writer.writeAll("this is likely due to the size of the combined lengths of the device/face names of all FONT resources"),
699 .hint => return,
700 },
701 .number_expression_as_filename => switch (self.type) {
702 .err, .warning => return writer.writeAll("filename cannot be specified using a number expression, consider using a quoted string instead"),
703 .note => return writer.print("the Win32 RC compiler would evaluate this number expression as the filename '{s}'", .{strings[self.extra.number]}),
704 .hint => return,
705 },
706 .control_id_already_defined => switch (self.type) {
707 .err, .warning => return writer.print("control with id {d} already defined for this dialog", .{self.extra.number}),
708 .note => return writer.print("previous definition of control with id {d} here", .{self.extra.number}),
709 .hint => return,
710 },
711 .invalid_filename => {
712 const disallowed_codepoint = self.extra.number;
713 if (disallowed_codepoint < 128 and std.ascii.isPrint(@intCast(disallowed_codepoint))) {
714 try writer.print("evaluated filename contains a disallowed character: '{c}'", .{@as(u8, @intCast(disallowed_codepoint))});
715 } else {
716 try writer.print("evaluated filename contains a disallowed codepoint: <U+{X:0>4}>", .{disallowed_codepoint});
717 }
718 },
719 .rc_would_error_u16_with_l_suffix => switch (self.type) {
720 .err, .warning => return writer.print("this {s} parameter would be an error in the Win32 RC compiler", .{@tagName(self.extra.statement_with_u16_param)}),
721 .note => return writer.writeAll("to avoid the error, remove any L suffixes from numbers within the parameter"),
722 .hint => return,
723 },
724 .result_contains_fontdir => return,
725 .rc_would_miscompile_dialog_menu_id => switch (self.type) {
726 .err, .warning => return writer.print("the id of this menu would be miscompiled by the Win32 RC compiler", .{}),
727 .note => return writer.print("the Win32 RC compiler would evaluate the id as the ordinal/number value {d}", .{self.extra.number}),
728 .hint => return,
729 },
730 .rc_would_miscompile_dialog_class => switch (self.type) {
731 .err, .warning => return writer.print("this class would be miscompiled by the Win32 RC compiler", .{}),
732 .note => return writer.print("the Win32 RC compiler would evaluate it as the ordinal/number value {d}", .{self.extra.number}),
733 .hint => return,
734 },
735 .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal => switch (self.type) {
736 .err, .warning => return,
737 .note => return writer.print("to avoid the potential miscompilation, only specify one {s} per dialog resource", .{@tagName(self.extra.menu_or_class)}),
738 .hint => return,
739 },
740 .rc_would_miscompile_dialog_menu_id_starts_with_digit => switch (self.type) {
741 .err, .warning => return,
742 .note => return writer.writeAll("to avoid the potential miscompilation, the first character of the id should not be a digit"),
743 .hint => return,
744 },
745 .dialog_menu_id_was_uppercased => return,
746 .duplicate_menu_or_class_skipped => {
747 return writer.print("this {s} was ignored; when multiple {s} statements are specified, only the last takes precedence", .{
748 @tagName(self.extra.menu_or_class),
749 @tagName(self.extra.menu_or_class),
750 });
751 },
752 .invalid_digit_character_in_ordinal => {
753 return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values");
754 },
755 .rc_would_miscompile_codepoint_byte_swap => switch (self.type) {
756 .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}),
757 .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}),
758 .hint => return,
759 },
760 .rc_would_miscompile_codepoint_skip => switch (self.type) {
761 .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}),
762 .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}),
763 .hint => return,
764 },
765 .tab_converted_to_spaces => switch (self.type) {
766 .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)"),
767 .note => return writer.writeAll("to include the tab character itself in a string, the escape sequence \\t should be used"),
768 .hint => return,
769 },
770 .win32_non_ascii_ordinal => switch (self.type) {
771 .err, .warning => unreachable,
772 .note => return writer.print("the Win32 RC compiler would accept this as an ordinal but its value would be {}", .{self.extra.number}),
773 .hint => return,
774 },
775 .failed_to_open_cwd => {
776 try writer.print("failed to open CWD for compilation: {s}", .{@tagName(self.extra.file_open_error.err)});
777 },
778 }
779 }
780
781 pub const VisualTokenInfo = struct {
782 before_len: usize,
783 point_offset: usize,
784 after_len: usize,
785 };
786
787 pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize) VisualTokenInfo {
788 // Note: A perfect solution here would involve full grapheme cluster
789 // awareness, but oh well. This will give incorrect offsets
790 // if there are any multibyte codepoints within the relevant span,
791 // and even more inflated for grapheme clusters.
792 //
793 // We mitigate this slightly when we know we'll be pointing at
794 // something that displays as 1 character.
795 return switch (self.err) {
796 // These can technically be more than 1 byte depending on encoding,
797 // but they always refer to one visual character/grapheme.
798 .illegal_byte,
799 .illegal_byte_outside_string_literals,
800 .illegal_codepoint_outside_string_literals,
801 .illegal_byte_order_mark,
802 .illegal_private_use_character,
803 => .{
804 .before_len = 0,
805 .point_offset = self.token.start - source_line_start,
806 .after_len = 0,
807 },
808 else => .{
809 .before_len = before: {
810 const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start);
811 break :before self.token.start - start;
812 },
813 .point_offset = self.token.start - source_line_start,
814 .after_len = after: {
815 const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end);
816 // end may be less than start when pointing to EOF
817 if (end <= self.token.start) break :after 0;
818 break :after end - self.token.start - 1;
819 },
820 },
821 };
822 }
823};
824
825pub 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 {
826 if (err_details.type == .hint) return;
827
828 const source_line_start = err_details.token.getLineStart(source);
829 // Treat tab stops as 1 column wide for error display purposes,
830 // and add one to get a 1-based column
831 const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1;
832
833 const corresponding_span: ?SourceMappings.SourceSpan = if (source_mappings != null and source_mappings.?.has(err_details.token.line_number))
834 source_mappings.?.get(err_details.token.line_number)
835 else
836 null;
837 const corresponding_file: ?[]const u8 = if (source_mappings != null and corresponding_span != null)
838 source_mappings.?.files.get(corresponding_span.?.filename_offset)
839 else
840 null;
841
842 const err_line = if (corresponding_span) |span| span.start_line else err_details.token.line_number;
843
844 try tty_config.setColor(writer, .bold);
845 if (corresponding_file) |file| {
846 try writer.writeAll(file);
847 } else {
848 try tty_config.setColor(writer, .dim);
849 try writer.writeAll("<after preprocessor>");
850 try tty_config.setColor(writer, .reset);
851 try tty_config.setColor(writer, .bold);
852 }
853 try writer.print(":{d}:{d}: ", .{ err_line, column });
854 switch (err_details.type) {
855 .err => {
856 try tty_config.setColor(writer, .red);
857 try writer.writeAll("error: ");
858 },
859 .warning => {
860 try tty_config.setColor(writer, .yellow);
861 try writer.writeAll("warning: ");
862 },
863 .note => {
864 try tty_config.setColor(writer, .cyan);
865 try writer.writeAll("note: ");
866 },
867 .hint => unreachable,
868 }
869 try tty_config.setColor(writer, .reset);
870 try tty_config.setColor(writer, .bold);
871 try err_details.render(writer, source, strings);
872 try writer.writeByte('\n');
873 try tty_config.setColor(writer, .reset);
874
875 if (!err_details.print_source_line) {
876 try writer.writeByte('\n');
877 return;
878 }
879
880 const source_line = err_details.token.getLine(source, source_line_start);
881 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
882
883 // Need this to determine if the 'line originated from' note is worth printing
884 var source_line_for_display_buf = try std.ArrayList(u8).initCapacity(allocator, source_line.len);
885 defer source_line_for_display_buf.deinit();
886 try writeSourceSlice(source_line_for_display_buf.writer(), source_line);
887
888 // TODO: General handling of long lines, not tied to this specific error
889 if (err_details.err == .string_literal_too_long) {
890 const before_slice = source_line[0..@min(source_line.len, visual_info.point_offset + 16)];
891 try writeSourceSlice(writer, before_slice);
892 try tty_config.setColor(writer, .dim);
893 try writer.writeAll("<...truncated...>");
894 try tty_config.setColor(writer, .reset);
895 } else {
896 try writer.writeAll(source_line_for_display_buf.items);
897 }
898 try writer.writeByte('\n');
899
900 try tty_config.setColor(writer, .green);
901 const num_spaces = visual_info.point_offset - visual_info.before_len;
902 try writer.writeByteNTimes(' ', num_spaces);
903 try writer.writeByteNTimes('~', visual_info.before_len);
904 try writer.writeByte('^');
905 if (visual_info.after_len > 0) {
906 var num_squiggles = visual_info.after_len;
907 if (err_details.err == .string_literal_too_long) {
908 num_squiggles = @min(num_squiggles, 15);
909 }
910 try writer.writeByteNTimes('~', num_squiggles);
911 }
912 try writer.writeByte('\n');
913 try tty_config.setColor(writer, .reset);
914
915 if (corresponding_span != null and corresponding_file != null) {
916 var corresponding_lines = try CorrespondingLines.init(allocator, cwd, err_details, source_line_for_display_buf.items, corresponding_span.?, corresponding_file.?);
917 defer corresponding_lines.deinit(allocator);
918
919 if (!corresponding_lines.worth_printing_note) return;
920
921 try tty_config.setColor(writer, .bold);
922 if (corresponding_file) |file| {
923 try writer.writeAll(file);
924 } else {
925 try tty_config.setColor(writer, .dim);
926 try writer.writeAll("<after preprocessor>");
927 try tty_config.setColor(writer, .reset);
928 try tty_config.setColor(writer, .bold);
929 }
930 try writer.print(":{d}:{d}: ", .{ err_line, column });
931 try tty_config.setColor(writer, .cyan);
932 try writer.writeAll("note: ");
933 try tty_config.setColor(writer, .reset);
934 try tty_config.setColor(writer, .bold);
935 try writer.writeAll("this line originated from line");
936 if (corresponding_span.?.start_line != corresponding_span.?.end_line) {
937 try writer.print("s {}-{}", .{ corresponding_span.?.start_line, corresponding_span.?.end_line });
938 } else {
939 try writer.print(" {}", .{corresponding_span.?.start_line});
940 }
941 try writer.print(" of file '{s}'\n", .{corresponding_file.?});
942 try tty_config.setColor(writer, .reset);
943
944 if (!corresponding_lines.worth_printing_lines) return;
945
946 if (corresponding_lines.lines_is_error_message) {
947 try tty_config.setColor(writer, .red);
948 try writer.writeAll(" | ");
949 try tty_config.setColor(writer, .reset);
950 try tty_config.setColor(writer, .dim);
951 try writer.writeAll(corresponding_lines.lines.items);
952 try tty_config.setColor(writer, .reset);
953 try writer.writeAll("\n\n");
954 return;
955 }
956
957 try writer.writeAll(corresponding_lines.lines.items);
958 try writer.writeAll("\n\n");
959 }
960}
961
962const CorrespondingLines = struct {
963 worth_printing_note: bool = true,
964 worth_printing_lines: bool = true,
965 lines: std.ArrayListUnmanaged(u8) = .{},
966 lines_is_error_message: bool = false,
967
968 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 {
969 var corresponding_lines = CorrespondingLines{};
970
971 // We don't do line comparison for this error, so don't print the note if the line
972 // number is different
973 if (err_details.err == .string_literal_too_long and err_details.token.line_number == corresponding_span.start_line) {
974 corresponding_lines.worth_printing_note = false;
975 return corresponding_lines;
976 }
977
978 // Don't print the originating line for this error, we know it's really long
979 if (err_details.err == .string_literal_too_long) {
980 corresponding_lines.worth_printing_lines = false;
981 return corresponding_lines;
982 }
983
984 var writer = corresponding_lines.lines.writer(allocator);
985 if (utils.openFileNotDir(cwd, corresponding_file, .{})) |file| {
986 defer file.close();
987 var buffered_reader = std.io.bufferedReader(file.reader());
988 writeLinesFromStream(writer, buffered_reader.reader(), corresponding_span.start_line, corresponding_span.end_line) catch |err| switch (err) {
989 error.LinesNotFound => {
990 corresponding_lines.lines.clearRetainingCapacity();
991 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});
992 corresponding_lines.lines_is_error_message = true;
993 return corresponding_lines;
994 },
995 else => |e| return e,
996 };
997 } else |err| {
998 corresponding_lines.lines.clearRetainingCapacity();
999 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});
1000 corresponding_lines.lines_is_error_message = true;
1001 return corresponding_lines;
1002 }
1003
1004 // If the lines are the same as they were before preprocessing, skip printing the note entirely
1005 if (std.mem.eql(u8, lines_for_comparison, corresponding_lines.lines.items)) {
1006 corresponding_lines.worth_printing_note = false;
1007 }
1008 return corresponding_lines;
1009 }
1010
1011 pub fn deinit(self: *CorrespondingLines, allocator: std.mem.Allocator) void {
1012 self.lines.deinit(allocator);
1013 }
1014};
1015
1016fn writeSourceSlice(writer: anytype, slice: []const u8) !void {
1017 for (slice) |c| try writeSourceByte(writer, c);
1018}
1019
1020inline fn writeSourceByte(writer: anytype, byte: u8) !void {
1021 switch (byte) {
1022 '\x00'...'\x08', '\x0E'...'\x1F', '\x7F' => try writer.writeAll("�"),
1023 // \r is seemingly ignored by the RC compiler so skipping it when printing source lines
1024 // could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up
1025 // in the console as DATA but the compiler reads it as RCDATA)
1026 //
1027 // NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r
1028 // characters get converted to \n, but may become relevant if another
1029 // preprocessor is used instead.
1030 '\r' => {},
1031 '\t', '\x0B', '\x0C' => try writer.writeByte(' '),
1032 else => try writer.writeByte(byte),
1033 }
1034}
1035
1036pub fn writeLinesFromStream(writer: anytype, input: anytype, start_line: usize, end_line: usize) !void {
1037 var line_num: usize = 1;
1038 while (try readByteOrEof(input)) |byte| {
1039 switch (byte) {
1040 '\n' => {
1041 if (line_num == end_line) return;
1042 if (line_num >= start_line) try writeSourceByte(writer, byte);
1043 line_num += 1;
1044 },
1045 else => {
1046 if (line_num >= start_line) try writeSourceByte(writer, byte);
1047 },
1048 }
1049 }
1050 if (line_num != end_line) {
1051 return error.LinesNotFound;
1052 }
1053}
1054
1055pub fn readByteOrEof(reader: anytype) !?u8 {
1056 return reader.readByte() catch |err| switch (err) {
1057 error.EndOfStream => return null,
1058 else => |e| return e,
1059 };
1060}
src/resinator/ico.zig deleted-312
...@@ -1,312 +0,0 @@
1//! https://devblogs.microsoft.com/oldnewthing/20120720-00/?p=7083
2//! https://learn.microsoft.com/en-us/previous-versions/ms997538(v=msdn.10)
3//! https://learn.microsoft.com/en-us/windows/win32/menurc/newheader
4//! https://learn.microsoft.com/en-us/windows/win32/menurc/resdir
5//! https://learn.microsoft.com/en-us/windows/win32/menurc/localheader
6
7const std = @import("std");
8const builtin = @import("builtin");
9const native_endian = builtin.cpu.arch.endian();
10
11pub const ReadError = std.mem.Allocator.Error || error{ InvalidHeader, InvalidImageType, ImpossibleDataSize, UnexpectedEOF, ReadError };
12
13pub fn read(allocator: std.mem.Allocator, reader: anytype, max_size: u64) ReadError!IconDir {
14 // Some Reader implementations have an empty ReadError error set which would
15 // cause 'unreachable else' if we tried to use an else in the switch, so we
16 // need to detect this case and not try to translate to ReadError
17 const empty_reader_errorset = @typeInfo(@TypeOf(reader).Error).ErrorSet == null or @typeInfo(@TypeOf(reader).Error).ErrorSet.?.len == 0;
18 if (empty_reader_errorset) {
19 return readAnyError(allocator, reader, max_size) catch |err| switch (err) {
20 error.EndOfStream => error.UnexpectedEOF,
21 else => |e| return e,
22 };
23 } else {
24 return readAnyError(allocator, reader, max_size) catch |err| switch (err) {
25 error.OutOfMemory,
26 error.InvalidHeader,
27 error.InvalidImageType,
28 error.ImpossibleDataSize,
29 => |e| return e,
30 error.EndOfStream => error.UnexpectedEOF,
31 // The remaining errors are dependent on the `reader`, so
32 // we just translate them all to generic ReadError
33 else => error.ReadError,
34 };
35 }
36}
37
38// TODO: This seems like a somewhat strange pattern, could be a better way
39// to do this. Maybe it makes more sense to handle the translation
40// at the call site instead of having a helper function here.
41pub fn readAnyError(allocator: std.mem.Allocator, reader: anytype, max_size: u64) !IconDir {
42 const reserved = try reader.readInt(u16, .little);
43 if (reserved != 0) {
44 return error.InvalidHeader;
45 }
46
47 const image_type = reader.readEnum(ImageType, .little) catch |err| switch (err) {
48 error.InvalidValue => return error.InvalidImageType,
49 else => |e| return e,
50 };
51
52 const num_images = try reader.readInt(u16, .little);
53
54 // To avoid over-allocation in the case of a file that says it has way more
55 // entries than it actually does, we use an ArrayList with a conservatively
56 // limited initial capacity instead of allocating the entire slice at once.
57 const initial_capacity = @min(num_images, 8);
58 var entries = try std.ArrayList(Entry).initCapacity(allocator, initial_capacity);
59 errdefer entries.deinit();
60
61 var i: usize = 0;
62 while (i < num_images) : (i += 1) {
63 var entry: Entry = undefined;
64 entry.width = try reader.readByte();
65 entry.height = try reader.readByte();
66 entry.num_colors = try reader.readByte();
67 entry.reserved = try reader.readByte();
68 switch (image_type) {
69 .icon => {
70 entry.type_specific_data = .{ .icon = .{
71 .color_planes = try reader.readInt(u16, .little),
72 .bits_per_pixel = try reader.readInt(u16, .little),
73 } };
74 },
75 .cursor => {
76 entry.type_specific_data = .{ .cursor = .{
77 .hotspot_x = try reader.readInt(u16, .little),
78 .hotspot_y = try reader.readInt(u16, .little),
79 } };
80 },
81 }
82 entry.data_size_in_bytes = try reader.readInt(u32, .little);
83 entry.data_offset_from_start_of_file = try reader.readInt(u32, .little);
84 // Validate that the offset/data size is feasible
85 if (@as(u64, entry.data_offset_from_start_of_file) + entry.data_size_in_bytes > max_size) {
86 return error.ImpossibleDataSize;
87 }
88 // and that the data size is large enough for at least the header of an image
89 // Note: This avoids needing to deal with a miscompilation from the Win32 RC
90 // compiler when the data size of an image is specified as zero but there
91 // is data to-be-read at the offset. The Win32 RC compiler will output
92 // an ICON/CURSOR resource with a bogus size in its header but with no actual
93 // data bytes in it, leading to an invalid .res. Similarly, if, for example,
94 // there is valid PNG data at the image's offset, but the size is specified
95 // as fewer bytes than the PNG header, then the Win32 RC compiler will still
96 // treat it as a PNG (e.g. unconditionally set num_planes to 1) but the data
97 // of the resource will only be 1 byte so treating it as a PNG doesn't make
98 // sense (especially not when you have to read past the data size to determine
99 // that it's a PNG).
100 if (entry.data_size_in_bytes < 16) {
101 return error.ImpossibleDataSize;
102 }
103 try entries.append(entry);
104 }
105
106 return .{
107 .image_type = image_type,
108 .entries = try entries.toOwnedSlice(),
109 .allocator = allocator,
110 };
111}
112
113pub const ImageType = enum(u16) {
114 icon = 1,
115 cursor = 2,
116};
117
118pub const IconDir = struct {
119 image_type: ImageType,
120 /// Note: entries.len will always fit into a u16, since the field containing the
121 /// number of images in an ico file is a u16.
122 entries: []Entry,
123 allocator: std.mem.Allocator,
124
125 pub fn deinit(self: IconDir) void {
126 self.allocator.free(self.entries);
127 }
128
129 pub const res_header_byte_len = 6;
130
131 pub fn getResDataSize(self: IconDir) u32 {
132 // maxInt(u16) * Entry.res_byte_len = 917,490 which is well within the u32 range.
133 // Note: self.entries.len is limited to maxInt(u16)
134 return @intCast(IconDir.res_header_byte_len + self.entries.len * Entry.res_byte_len);
135 }
136
137 pub fn writeResData(self: IconDir, writer: anytype, first_image_id: u16) !void {
138 try writer.writeInt(u16, 0, .little);
139 try writer.writeInt(u16, @intFromEnum(self.image_type), .little);
140 // We know that entries.len must fit into a u16
141 try writer.writeInt(u16, @as(u16, @intCast(self.entries.len)), .little);
142
143 var image_id = first_image_id;
144 for (self.entries) |entry| {
145 try entry.writeResData(writer, image_id);
146 image_id += 1;
147 }
148 }
149};
150
151pub const Entry = struct {
152 // Icons are limited to u8 sizes, cursors can have u16,
153 // so we store as u16 and truncate when needed.
154 width: u16,
155 height: u16,
156 num_colors: u8,
157 /// This should always be zero, but whatever value it is gets
158 /// carried over so we need to store it
159 reserved: u8,
160 type_specific_data: union(ImageType) {
161 icon: struct {
162 color_planes: u16,
163 bits_per_pixel: u16,
164 },
165 cursor: struct {
166 hotspot_x: u16,
167 hotspot_y: u16,
168 },
169 },
170 data_size_in_bytes: u32,
171 data_offset_from_start_of_file: u32,
172
173 pub const res_byte_len = 14;
174
175 pub fn writeResData(self: Entry, writer: anytype, id: u16) !void {
176 switch (self.type_specific_data) {
177 .icon => |icon_data| {
178 try writer.writeInt(u8, @as(u8, @truncate(self.width)), .little);
179 try writer.writeInt(u8, @as(u8, @truncate(self.height)), .little);
180 try writer.writeInt(u8, self.num_colors, .little);
181 try writer.writeInt(u8, self.reserved, .little);
182 try writer.writeInt(u16, icon_data.color_planes, .little);
183 try writer.writeInt(u16, icon_data.bits_per_pixel, .little);
184 try writer.writeInt(u32, self.data_size_in_bytes, .little);
185 },
186 .cursor => |cursor_data| {
187 try writer.writeInt(u16, self.width, .little);
188 try writer.writeInt(u16, self.height, .little);
189 try writer.writeInt(u16, cursor_data.hotspot_x, .little);
190 try writer.writeInt(u16, cursor_data.hotspot_y, .little);
191 try writer.writeInt(u32, self.data_size_in_bytes + 4, .little);
192 },
193 }
194 try writer.writeInt(u16, id, .little);
195 }
196};
197
198test "icon" {
199 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;
200 var fbs = std.io.fixedBufferStream(data);
201 const icon = try read(std.testing.allocator, fbs.reader(), data.len);
202 defer icon.deinit();
203
204 try std.testing.expectEqual(ImageType.icon, icon.image_type);
205 try std.testing.expectEqual(@as(usize, 1), icon.entries.len);
206}
207
208test "icon too many images" {
209 // Note that with verifying that all data sizes are within the file bounds and >= 16,
210 // it's not possible to hit EOF when looking for more RESDIR structures, since they are
211 // themselves 16 bytes long, so we'll always hit ImpossibleDataSize instead.
212 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;
213 var fbs = std.io.fixedBufferStream(data);
214 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
215}
216
217test "icon data size past EOF" {
218 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;
219 var fbs = std.io.fixedBufferStream(data);
220 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
221}
222
223test "icon data offset past EOF" {
224 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;
225 var fbs = std.io.fixedBufferStream(data);
226 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
227}
228
229test "icon data size too small" {
230 const data = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00\x10\x00\x0F\x00\x00\x00\x16\x00\x00\x00";
231 var fbs = std.io.fixedBufferStream(data);
232 try std.testing.expectError(error.ImpossibleDataSize, read(std.testing.allocator, fbs.reader(), data.len));
233}
234
235pub const ImageFormat = enum {
236 dib,
237 png,
238 riff,
239
240 const riff_header = std.mem.readInt(u32, "RIFF", native_endian);
241 const png_signature = std.mem.readInt(u64, "\x89PNG\r\n\x1a\n", native_endian);
242 const ihdr_code = std.mem.readInt(u32, "IHDR", native_endian);
243 const acon_form_type = std.mem.readInt(u32, "ACON", native_endian);
244
245 pub fn detect(header_bytes: *const [16]u8) ImageFormat {
246 if (std.mem.readInt(u32, header_bytes[0..4], native_endian) == riff_header) return .riff;
247 if (std.mem.readInt(u64, header_bytes[0..8], native_endian) == png_signature) return .png;
248 return .dib;
249 }
250
251 pub fn validate(format: ImageFormat, header_bytes: *const [16]u8) bool {
252 return switch (format) {
253 .png => std.mem.readInt(u32, header_bytes[12..16], native_endian) == ihdr_code,
254 .riff => std.mem.readInt(u32, header_bytes[8..12], native_endian) == acon_form_type,
255 .dib => true,
256 };
257 }
258};
259
260/// Contains only the fields of BITMAPINFOHEADER (WinGDI.h) that are both:
261/// - relevant to what we need, and
262/// - are shared between all versions of BITMAPINFOHEADER (V4, V5).
263pub const BitmapHeader = extern struct {
264 bcSize: u32,
265 bcWidth: i32,
266 bcHeight: i32,
267 bcPlanes: u16,
268 bcBitCount: u16,
269
270 pub fn version(self: *const BitmapHeader) Version {
271 return Version.get(self.bcSize);
272 }
273
274 /// https://en.wikipedia.org/wiki/BMP_file_format#DIB_header_(bitmap_information_header)
275 pub const Version = enum {
276 unknown,
277 @"win2.0", // Windows 2.0 or later
278 @"nt3.1", // Windows NT, 3.1x or later
279 @"nt4.0", // Windows NT 4.0, 95 or later
280 @"nt5.0", // Windows NT 5.0, 98 or later
281
282 pub fn get(header_size: u32) Version {
283 return switch (header_size) {
284 len(.@"win2.0") => .@"win2.0",
285 len(.@"nt3.1") => .@"nt3.1",
286 len(.@"nt4.0") => .@"nt4.0",
287 len(.@"nt5.0") => .@"nt5.0",
288 else => .unknown,
289 };
290 }
291
292 pub fn len(comptime v: Version) comptime_int {
293 return switch (v) {
294 .@"win2.0" => 12,
295 .@"nt3.1" => 40,
296 .@"nt4.0" => 108,
297 .@"nt5.0" => 124,
298 .unknown => unreachable,
299 };
300 }
301
302 pub fn nameForErrorDisplay(v: Version) []const u8 {
303 return switch (v) {
304 .unknown => "unknown",
305 .@"win2.0" => "Windows 2.0 (BITMAPCOREHEADER)",
306 .@"nt3.1" => "Windows NT, 3.1x (BITMAPINFOHEADER)",
307 .@"nt4.0" => "Windows NT 4.0, 95 (BITMAPV4HEADER)",
308 .@"nt5.0" => "Windows NT 5.0, 98 (BITMAPV5HEADER)",
309 };
310 }
311 };
312};
src/resinator/lang.zig deleted-877
...@@ -1,877 +0,0 @@
1const std = @import("std");
2
3/// This function is specific to how the Win32 RC command line interprets
4/// language IDs specified as integers.
5/// - Always interpreted as hexadecimal, but explicit 0x prefix is also allowed
6/// - Wraps on overflow of u16
7/// - Stops parsing on any invalid hexadecimal digits
8/// - Errors if a digit is not the first char
9/// - `-` (negative) prefix is allowed
10pub fn parseInt(str: []const u8) error{InvalidLanguageId}!u16 {
11 var result: u16 = 0;
12 const radix: u8 = 16;
13 var buf = str;
14
15 const Prefix = enum { none, minus };
16 var prefix: Prefix = .none;
17 switch (buf[0]) {
18 '-' => {
19 prefix = .minus;
20 buf = buf[1..];
21 },
22 else => {},
23 }
24
25 if (buf.len > 2 and buf[0] == '0' and buf[1] == 'x') {
26 buf = buf[2..];
27 }
28
29 for (buf, 0..) |c, i| {
30 const digit = switch (c) {
31 // On invalid digit for the radix, just stop parsing but don't fail
32 'a'...'f', 'A'...'F', '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
33 else => {
34 // First digit must be valid
35 if (i == 0) {
36 return error.InvalidLanguageId;
37 }
38 break;
39 },
40 };
41
42 if (result != 0) {
43 result *%= radix;
44 }
45 result +%= digit;
46 }
47
48 switch (prefix) {
49 .none => {},
50 .minus => result = 0 -% result,
51 }
52
53 return result;
54}
55
56test parseInt {
57 try std.testing.expectEqual(@as(u16, 0x16), try parseInt("16"));
58 try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1A"));
59 try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1Azzzz"));
60 try std.testing.expectEqual(@as(u16, 0xffff), try parseInt("-1"));
61 try std.testing.expectEqual(@as(u16, 0xffea), try parseInt("-0x16"));
62 try std.testing.expectEqual(@as(u16, 0x0), try parseInt("0o100"));
63 try std.testing.expectEqual(@as(u16, 0x1), try parseInt("10001"));
64 try std.testing.expectError(error.InvalidLanguageId, parseInt("--1"));
65 try std.testing.expectError(error.InvalidLanguageId, parseInt("0xha"));
66 try std.testing.expectError(error.InvalidLanguageId, parseInt("¹"));
67 try std.testing.expectError(error.InvalidLanguageId, parseInt("~1"));
68}
69
70/// This function is specific to how the Win32 RC command line interprets
71/// language tags: invalid tags are rejected, but tags that don't have
72/// a specific assigned ID but are otherwise valid enough will get
73/// converted to an ID of LOCALE_CUSTOM_UNSPECIFIED.
74pub fn tagToInt(tag: []const u8) error{InvalidLanguageTag}!u16 {
75 const maybe_id = try tagToId(tag);
76 if (maybe_id) |id| {
77 return @intFromEnum(id);
78 } else {
79 return LOCALE_CUSTOM_UNSPECIFIED;
80 }
81}
82
83pub fn tagToId(tag: []const u8) error{InvalidLanguageTag}!?LanguageId {
84 const parsed = try parse(tag);
85 // There are currently no language tags with assigned IDs that have
86 // multiple suffixes, so we can skip the lookup.
87 if (parsed.multiple_suffixes) return null;
88 const longest_known_tag = comptime blk: {
89 var len = 0;
90 for (@typeInfo(LanguageId).Enum.fields) |field| {
91 if (field.name.len > len) len = field.name.len;
92 }
93 break :blk len;
94 };
95 // If the tag is longer than the longest tag that has an assigned ID,
96 // then we can skip the lookup.
97 if (tag.len > longest_known_tag) return null;
98 var normalized_buf: [longest_known_tag]u8 = undefined;
99 // To allow e.g. `de-de_phoneb` to get looked up as `de-de`, we need to
100 // omit the suffix, but only if the tag contains a valid alternate sort order.
101 const tag_to_normalize = if (parsed.isSuffixValidSortOrder()) tag[0 .. tag.len - (parsed.suffix.?.len + 1)] else tag;
102 const normalized_tag = normalizeTag(tag_to_normalize, &normalized_buf);
103 return std.meta.stringToEnum(LanguageId, normalized_tag) orelse {
104 // special case for a tag that has been mapped to the same ID
105 // twice.
106 if (std.mem.eql(u8, "ff_latn_ng", normalized_tag)) {
107 return LanguageId.ff_ng;
108 }
109 return null;
110 };
111}
112
113test tagToId {
114 try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("ar-ae")).?);
115 try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("AR_AE")).?);
116 try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-ng")).?);
117 // Special case
118 try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-Latn-NG")).?);
119}
120
121test "exhaustive tagToId" {
122 inline for (@typeInfo(LanguageId).Enum.fields) |field| {
123 const id = tagToId(field.name) catch |err| {
124 std.debug.print("tag: {s}\n", .{field.name});
125 return err;
126 };
127 try std.testing.expectEqual(@field(LanguageId, field.name), id orelse {
128 std.debug.print("tag: {s}, got null\n", .{field.name});
129 return error.TestExpectedEqual;
130 });
131 }
132 var buf: [32]u8 = undefined;
133 inline for (valid_alternate_sorts) |parsed_sort| {
134 var fbs = std.io.fixedBufferStream(&buf);
135 const writer = fbs.writer();
136 writer.writeAll(parsed_sort.language_code) catch unreachable;
137 writer.writeAll("-") catch unreachable;
138 writer.writeAll(parsed_sort.country_code.?) catch unreachable;
139 writer.writeAll("-") catch unreachable;
140 writer.writeAll(parsed_sort.suffix.?) catch unreachable;
141 const expected_field_name = comptime field: {
142 var name_buf: [5]u8 = undefined;
143 @memcpy(&name_buf[0..parsed_sort.language_code.len], parsed_sort.language_code);
144 name_buf[2] = '_';
145 @memcpy(name_buf[3..], parsed_sort.country_code.?);
146 break :field name_buf;
147 };
148 const expected = @field(LanguageId, &expected_field_name);
149 const id = tagToId(fbs.getWritten()) catch |err| {
150 std.debug.print("tag: {s}\n", .{fbs.getWritten()});
151 return err;
152 };
153 try std.testing.expectEqual(expected, id orelse {
154 std.debug.print("tag: {s}, expected: {}, got null\n", .{ fbs.getWritten(), expected });
155 return error.TestExpectedEqual;
156 });
157 }
158}
159
160fn normalizeTag(tag: []const u8, buf: []u8) []u8 {
161 std.debug.assert(buf.len >= tag.len);
162 for (tag, 0..) |c, i| {
163 if (c == '-')
164 buf[i] = '_'
165 else
166 buf[i] = std.ascii.toLower(c);
167 }
168 return buf[0..tag.len];
169}
170
171/// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-LCID/%5bMS-LCID%5d.pdf#%5B%7B%22num%22%3A72%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C69%2C574%2C0%5D
172/// "When an LCID is requested for a locale without a
173/// permanent LCID assignment, nor a temporary
174/// assignment as above, the protocol will respond
175/// with LOCALE_CUSTOM_UNSPECIFIED for all such
176/// locales. Because this single value is used for
177/// numerous possible locale names, it is impossible to
178/// round trip this locale, even temporarily.
179/// Applications should discard this value as soon as
180/// possible and never persist it. If the system is
181/// forced to respond to a request for
182/// LCID_CUSTOM_UNSPECIFIED, it will fall back to
183/// the current user locale. This is often incorrect but
184/// may prevent an application or component from
185/// failing. As the meaning of this temporary LCID is
186/// unstable, it should never be used for interchange
187/// or persisted data. This is a 1-to-many relationship
188/// that is very unstable."
189pub const LOCALE_CUSTOM_UNSPECIFIED = 0x1000;
190
191pub const LANG_ENGLISH = 0x09;
192pub const SUBLANG_ENGLISH_US = 0x01;
193
194/// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers
195pub fn MAKELANGID(primary: u10, sublang: u6) u16 {
196 return (@as(u16, primary) << 10) | sublang;
197}
198
199/// Language tag format expressed as a regular expression (rough approximation):
200///
201/// [a-zA-Z]{1,3}([-_][a-zA-Z]{4})?([-_][a-zA-Z]{2})?([-_][a-zA-Z0-9]{1,8})?
202/// lang | script | country | suffix
203///
204/// Notes:
205/// - If lang code is 1 char, it seems to mean that everything afterwards uses suffix
206/// parsing rules (e.g. `a-0` and `a-00000000` are allowed).
207/// - There can also be any number of trailing suffix parts as long as they each
208/// would be a valid suffix part, e.g. `en-us-blah-blah1-blah2-blah3` is allowed.
209/// - When doing lookups, trailing suffix parts are taken into account, e.g.
210/// `ca-es-valencia` is not considered equivalent to `ca-es-valencia-blah`.
211/// - A suffix is only allowed if:
212/// + Lang code is 1 char long, or
213/// + A country code is present, or
214/// + A script tag is not present and:
215/// - the suffix is numeric-only and has a length of 3, or
216/// - the lang is `qps` and the suffix is `ploca` or `plocm`
217pub fn parse(lang_tag: []const u8) error{InvalidLanguageTag}!Parsed {
218 var it = std.mem.splitAny(u8, lang_tag, "-_");
219 const lang_code = it.first();
220 const is_valid_lang_code = lang_code.len >= 1 and lang_code.len <= 3 and isAllAlphabetic(lang_code);
221 if (!is_valid_lang_code) return error.InvalidLanguageTag;
222 var parsed = Parsed{
223 .language_code = lang_code,
224 };
225 // The second part could be a script tag, a country code, or a suffix
226 if (it.next()) |part_str| {
227 // The lang code being length 1 behaves strangely, so fully special case it.
228 if (lang_code.len == 1) {
229 // This is almost certainly not the 'right' way to do this, but I don't have a method
230 // to determine how exactly these language tags are parsed, and it seems like
231 // suffix parsing rules apply generally (digits allowed, length of 1 to 8).
232 //
233 // However, because we want to be able to lookup `x-iv-mathan` normally without
234 // `multiple_suffixes` being set to true, we need to make sure to treat two-length
235 // alphabetic parts as a country code.
236 if (part_str.len == 2 and isAllAlphabetic(part_str)) {
237 parsed.country_code = part_str;
238 }
239 // Everything else, though, we can just throw into the suffix as long as the normal
240 // rules apply.
241 else if (part_str.len > 0 and part_str.len <= 8 and isAllAlphanumeric(part_str)) {
242 parsed.suffix = part_str;
243 } else {
244 return error.InvalidLanguageTag;
245 }
246 } else if (part_str.len == 4 and isAllAlphabetic(part_str)) {
247 parsed.script_tag = part_str;
248 } else if (part_str.len == 2 and isAllAlphabetic(part_str)) {
249 parsed.country_code = part_str;
250 }
251 // Only a 3-len numeric suffix is allowed as the second part of a tag
252 else if (part_str.len == 3 and isAllNumeric(part_str)) {
253 parsed.suffix = part_str;
254 }
255 // Special case for qps-ploca and qps-plocm
256 else if (std.ascii.eqlIgnoreCase(lang_code, "qps") and
257 (std.ascii.eqlIgnoreCase(part_str, "ploca") or
258 std.ascii.eqlIgnoreCase(part_str, "plocm")))
259 {
260 parsed.suffix = part_str;
261 } else {
262 return error.InvalidLanguageTag;
263 }
264 } else {
265 // If there's no part besides a 1-len lang code, then it is malformed
266 if (lang_code.len == 1) return error.InvalidLanguageTag;
267 return parsed;
268 }
269 if (parsed.script_tag != null) {
270 if (it.next()) |part_str| {
271 if (part_str.len == 2 and isAllAlphabetic(part_str)) {
272 parsed.country_code = part_str;
273 } else {
274 // Suffix is not allowed when a country code is not present.
275 return error.InvalidLanguageTag;
276 }
277 } else {
278 return parsed;
279 }
280 }
281 // We've now parsed any potential script tag/country codes, so anything remaining
282 // is a suffix
283 while (it.next()) |part_str| {
284 if (part_str.len == 0 or part_str.len > 8 or !isAllAlphanumeric(part_str)) {
285 return error.InvalidLanguageTag;
286 }
287 if (parsed.suffix == null) {
288 parsed.suffix = part_str;
289 } else {
290 // In theory we could return early here but we still want to validate
291 // that each part is a valid suffix all the way to the end, e.g.
292 // we should reject `en-us-suffix-a-b-c-!!!` because of the invalid `!!!`
293 // suffix part.
294 parsed.multiple_suffixes = true;
295 }
296 }
297 return parsed;
298}
299
300pub const Parsed = struct {
301 language_code: []const u8,
302 script_tag: ?[]const u8 = null,
303 country_code: ?[]const u8 = null,
304 /// Can be a sort order (e.g. phoneb) or something like valencia, 001, etc
305 suffix: ?[]const u8 = null,
306 /// There can be any number of suffixes, but we don't need to care what their
307 /// values are, we just need to know if any exist so that e.g. `ca-es-valencia-blah`
308 /// can be seen as different from `ca-es-valencia`. Storing this as a bool
309 /// allows us to avoid needing either (a) dynamic allocation or (b) a limit to
310 /// the number of suffixes allowed when parsing.
311 multiple_suffixes: bool = false,
312
313 pub fn isSuffixValidSortOrder(self: Parsed) bool {
314 if (self.country_code == null) return false;
315 if (self.suffix == null) return false;
316 if (self.script_tag != null) return false;
317 if (self.multiple_suffixes) return false;
318 for (valid_alternate_sorts) |valid_sort| {
319 if (std.ascii.eqlIgnoreCase(valid_sort.language_code, self.language_code) and
320 std.ascii.eqlIgnoreCase(valid_sort.country_code.?, self.country_code.?) and
321 std.ascii.eqlIgnoreCase(valid_sort.suffix.?, self.suffix.?))
322 {
323 return true;
324 }
325 }
326 return false;
327 }
328};
329
330/// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
331/// See the table following this text: "Alternate sorts can be selected by using one of the identifiers from the following table."
332const valid_alternate_sorts = [_]Parsed{
333 // Note: x-IV-mathan is omitted due to how lookups are implemented.
334 // This table is used to make e.g. `de-de_phoneb` get looked up
335 // as `de-de` (the suffix is omitted for the lookup), but x-iv-mathan
336 // instead needs to be looked up with the suffix included because
337 // `x-iv` is not a tag with an assigned ID.
338 .{ .language_code = "de", .country_code = "de", .suffix = "phoneb" },
339 .{ .language_code = "hu", .country_code = "hu", .suffix = "tchncl" },
340 .{ .language_code = "ka", .country_code = "ge", .suffix = "modern" },
341 .{ .language_code = "zh", .country_code = "cn", .suffix = "stroke" },
342 .{ .language_code = "zh", .country_code = "sg", .suffix = "stroke" },
343 .{ .language_code = "zh", .country_code = "mo", .suffix = "stroke" },
344 .{ .language_code = "zh", .country_code = "tw", .suffix = "pronun" },
345 .{ .language_code = "zh", .country_code = "tw", .suffix = "radstr" },
346 .{ .language_code = "ja", .country_code = "jp", .suffix = "radstr" },
347 .{ .language_code = "zh", .country_code = "hk", .suffix = "radstr" },
348 .{ .language_code = "zh", .country_code = "mo", .suffix = "radstr" },
349 .{ .language_code = "zh", .country_code = "cn", .suffix = "phoneb" },
350 .{ .language_code = "zh", .country_code = "sg", .suffix = "phoneb" },
351};
352
353test "parse" {
354 try std.testing.expectEqualDeep(Parsed{
355 .language_code = "en",
356 }, try parse("en"));
357 try std.testing.expectEqualDeep(Parsed{
358 .language_code = "en",
359 .country_code = "us",
360 }, try parse("en-us"));
361 try std.testing.expectEqualDeep(Parsed{
362 .language_code = "en",
363 .suffix = "123",
364 }, try parse("en-123"));
365 try std.testing.expectEqualDeep(Parsed{
366 .language_code = "en",
367 .suffix = "123",
368 .multiple_suffixes = true,
369 }, try parse("en-123-blah"));
370 try std.testing.expectEqualDeep(Parsed{
371 .language_code = "en",
372 .country_code = "us",
373 .suffix = "123",
374 .multiple_suffixes = true,
375 }, try parse("en-us_123-blah"));
376 try std.testing.expectEqualDeep(Parsed{
377 .language_code = "eng",
378 .script_tag = "Latn",
379 }, try parse("eng-Latn"));
380 try std.testing.expectEqualDeep(Parsed{
381 .language_code = "eng",
382 .script_tag = "Latn",
383 }, try parse("eng-Latn"));
384 try std.testing.expectEqualDeep(Parsed{
385 .language_code = "ff",
386 .script_tag = "Latn",
387 .country_code = "NG",
388 }, try parse("ff-Latn-NG"));
389 try std.testing.expectEqualDeep(Parsed{
390 .language_code = "qps",
391 .suffix = "Plocm",
392 }, try parse("qps-Plocm"));
393 try std.testing.expectEqualDeep(Parsed{
394 .language_code = "qps",
395 .suffix = "ploca",
396 }, try parse("qps-ploca"));
397 try std.testing.expectEqualDeep(Parsed{
398 .language_code = "x",
399 .country_code = "IV",
400 .suffix = "mathan",
401 }, try parse("x-IV-mathan"));
402 try std.testing.expectEqualDeep(Parsed{
403 .language_code = "a",
404 .suffix = "a",
405 }, try parse("a-a"));
406 try std.testing.expectEqualDeep(Parsed{
407 .language_code = "a",
408 .suffix = "000",
409 }, try parse("a-000"));
410 try std.testing.expectEqualDeep(Parsed{
411 .language_code = "a",
412 .suffix = "00000000",
413 }, try parse("a-00000000"));
414 // suffix not allowed if script tag is present without country code
415 try std.testing.expectError(error.InvalidLanguageTag, parse("eng-Latn-suffix"));
416 // suffix must be 3 numeric digits if neither script tag nor country code is present
417 try std.testing.expectError(error.InvalidLanguageTag, parse("eng-suffix"));
418 try std.testing.expectError(error.InvalidLanguageTag, parse("en-plocm"));
419 // 1-len lang code is not allowed if it's the only part
420 try std.testing.expectError(error.InvalidLanguageTag, parse("e"));
421}
422
423fn isAllAlphabetic(str: []const u8) bool {
424 for (str) |c| {
425 if (!std.ascii.isAlphabetic(c)) return false;
426 }
427 return true;
428}
429
430fn isAllAlphanumeric(str: []const u8) bool {
431 for (str) |c| {
432 if (!std.ascii.isAlphanumeric(c)) return false;
433 }
434 return true;
435}
436
437fn isAllNumeric(str: []const u8) bool {
438 for (str) |c| {
439 if (!std.ascii.isDigit(c)) return false;
440 }
441 return true;
442}
443
444/// Derived from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
445/// - Protocol Revision: 15.0
446/// - Language / Language ID / Language Tag table in Appendix A
447/// - Removed all rows that have Language ID 0x1000 (LOCALE_CUSTOM_UNSPECIFIED)
448/// - Normalized each language tag (lowercased, replaced all `-` with `_`)
449/// - There is one special case where two tags are mapped to the same ID, the following
450/// has been omitted and must be special cased during lookup to map to the ID ff_ng / 0x0467.
451/// ff_latn_ng = 0x0467, // Fulah (Latin), Nigeria
452/// - x_iv_mathan has been added which is not in the table but does appear in the Alternate sorts
453/// table as 0x007F (LANG_INVARIANT).
454pub const LanguageId = enum(u16) {
455 // Language tag = Language ID, // Language, Location (or type)
456 af = 0x0036, // Afrikaans
457 af_za = 0x0436, // Afrikaans, South Africa
458 sq = 0x001C, // Albanian
459 sq_al = 0x041C, // Albanian, Albania
460 gsw = 0x0084, // Alsatian
461 gsw_fr = 0x0484, // Alsatian, France
462 am = 0x005E, // Amharic
463 am_et = 0x045E, // Amharic, Ethiopia
464 ar = 0x0001, // Arabic
465 ar_dz = 0x1401, // Arabic, Algeria
466 ar_bh = 0x3C01, // Arabic, Bahrain
467 ar_eg = 0x0c01, // Arabic, Egypt
468 ar_iq = 0x0801, // Arabic, Iraq
469 ar_jo = 0x2C01, // Arabic, Jordan
470 ar_kw = 0x3401, // Arabic, Kuwait
471 ar_lb = 0x3001, // Arabic, Lebanon
472 ar_ly = 0x1001, // Arabic, Libya
473 ar_ma = 0x1801, // Arabic, Morocco
474 ar_om = 0x2001, // Arabic, Oman
475 ar_qa = 0x4001, // Arabic, Qatar
476 ar_sa = 0x0401, // Arabic, Saudi Arabia
477 ar_sy = 0x2801, // Arabic, Syria
478 ar_tn = 0x1C01, // Arabic, Tunisia
479 ar_ae = 0x3801, // Arabic, U.A.E.
480 ar_ye = 0x2401, // Arabic, Yemen
481 hy = 0x002B, // Armenian
482 hy_am = 0x042B, // Armenian, Armenia
483 as = 0x004D, // Assamese
484 as_in = 0x044D, // Assamese, India
485 az_cyrl = 0x742C, // Azerbaijani (Cyrillic)
486 az_cyrl_az = 0x082C, // Azerbaijani (Cyrillic), Azerbaijan
487 az = 0x002C, // Azerbaijani (Latin)
488 az_latn = 0x782C, // Azerbaijani (Latin)
489 az_latn_az = 0x042C, // Azerbaijani (Latin), Azerbaijan
490 bn = 0x0045, // Bangla
491 bn_bd = 0x0845, // Bangla, Bangladesh
492 bn_in = 0x0445, // Bangla, India
493 ba = 0x006D, // Bashkir
494 ba_ru = 0x046D, // Bashkir, Russia
495 eu = 0x002D, // Basque
496 eu_es = 0x042D, // Basque, Spain
497 be = 0x0023, // Belarusian
498 be_by = 0x0423, // Belarusian, Belarus
499 bs_cyrl = 0x641A, // Bosnian (Cyrillic)
500 bs_cyrl_ba = 0x201A, // Bosnian (Cyrillic), Bosnia and Herzegovina
501 bs_latn = 0x681A, // Bosnian (Latin)
502 bs = 0x781A, // Bosnian (Latin)
503 bs_latn_ba = 0x141A, // Bosnian (Latin), Bosnia and Herzegovina
504 br = 0x007E, // Breton
505 br_fr = 0x047E, // Breton, France
506 bg = 0x0002, // Bulgarian
507 bg_bg = 0x0402, // Bulgarian, Bulgaria
508 my = 0x0055, // Burmese
509 my_mm = 0x0455, // Burmese, Myanmar
510 ca = 0x0003, // Catalan
511 ca_es = 0x0403, // Catalan, Spain
512 tzm_arab_ma = 0x045F, // Central Atlas Tamazight (Arabic), Morocco
513 ku = 0x0092, // Central Kurdish
514 ku_arab = 0x7c92, // Central Kurdish
515 ku_arab_iq = 0x0492, // Central Kurdish, Iraq
516 chr = 0x005C, // Cherokee
517 chr_cher = 0x7c5C, // Cherokee
518 chr_cher_us = 0x045C, // Cherokee, United States
519 zh_hans = 0x0004, // Chinese (Simplified)
520 zh = 0x7804, // Chinese (Simplified)
521 zh_cn = 0x0804, // Chinese (Simplified), People's Republic of China
522 zh_sg = 0x1004, // Chinese (Simplified), Singapore
523 zh_hant = 0x7C04, // Chinese (Traditional)
524 zh_hk = 0x0C04, // Chinese (Traditional), Hong Kong S.A.R.
525 zh_mo = 0x1404, // Chinese (Traditional), Macao S.A.R.
526 zh_tw = 0x0404, // Chinese (Traditional), Taiwan
527 co = 0x0083, // Corsican
528 co_fr = 0x0483, // Corsican, France
529 hr = 0x001A, // Croatian
530 hr_hr = 0x041A, // Croatian, Croatia
531 hr_ba = 0x101A, // Croatian (Latin), Bosnia and Herzegovina
532 cs = 0x0005, // Czech
533 cs_cz = 0x0405, // Czech, Czech Republic
534 da = 0x0006, // Danish
535 da_dk = 0x0406, // Danish, Denmark
536 prs = 0x008C, // Dari
537 prs_af = 0x048C, // Dari, Afghanistan
538 dv = 0x0065, // Divehi
539 dv_mv = 0x0465, // Divehi, Maldives
540 nl = 0x0013, // Dutch
541 nl_be = 0x0813, // Dutch, Belgium
542 nl_nl = 0x0413, // Dutch, Netherlands
543 dz_bt = 0x0C51, // Dzongkha, Bhutan
544 en = 0x0009, // English
545 en_au = 0x0C09, // English, Australia
546 en_bz = 0x2809, // English, Belize
547 en_ca = 0x1009, // English, Canada
548 en_029 = 0x2409, // English, Caribbean
549 en_hk = 0x3C09, // English, Hong Kong
550 en_in = 0x4009, // English, India
551 en_ie = 0x1809, // English, Ireland
552 en_jm = 0x2009, // English, Jamaica
553 en_my = 0x4409, // English, Malaysia
554 en_nz = 0x1409, // English, New Zealand
555 en_ph = 0x3409, // English, Republic of the Philippines
556 en_sg = 0x4809, // English, Singapore
557 en_za = 0x1C09, // English, South Africa
558 en_tt = 0x2c09, // English, Trinidad and Tobago
559 en_ae = 0x4C09, // English, United Arab Emirates
560 en_gb = 0x0809, // English, United Kingdom
561 en_us = 0x0409, // English, United States
562 en_zw = 0x3009, // English, Zimbabwe
563 et = 0x0025, // Estonian
564 et_ee = 0x0425, // Estonian, Estonia
565 fo = 0x0038, // Faroese
566 fo_fo = 0x0438, // Faroese, Faroe Islands
567 fil = 0x0064, // Filipino
568 fil_ph = 0x0464, // Filipino, Philippines
569 fi = 0x000B, // Finnish
570 fi_fi = 0x040B, // Finnish, Finland
571 fr = 0x000C, // French
572 fr_be = 0x080C, // French, Belgium
573 fr_cm = 0x2c0C, // French, Cameroon
574 fr_ca = 0x0c0C, // French, Canada
575 fr_029 = 0x1C0C, // French, Caribbean
576 fr_cd = 0x240C, // French, Congo, DRC
577 fr_ci = 0x300C, // French, Côte d'Ivoire
578 fr_fr = 0x040C, // French, France
579 fr_ht = 0x3c0C, // French, Haiti
580 fr_lu = 0x140C, // French, Luxembourg
581 fr_ml = 0x340C, // French, Mali
582 fr_ma = 0x380C, // French, Morocco
583 fr_mc = 0x180C, // French, Principality of Monaco
584 fr_re = 0x200C, // French, Reunion
585 fr_sn = 0x280C, // French, Senegal
586 fr_ch = 0x100C, // French, Switzerland
587 fy = 0x0062, // Frisian
588 fy_nl = 0x0462, // Frisian, Netherlands
589 ff = 0x0067, // Fulah
590 ff_latn = 0x7C67, // Fulah (Latin)
591 ff_ng = 0x0467, // Fulah, Nigeria
592 ff_latn_sn = 0x0867, // Fulah, Senegal
593 gl = 0x0056, // Galician
594 gl_es = 0x0456, // Galician, Spain
595 ka = 0x0037, // Georgian
596 ka_ge = 0x0437, // Georgian, Georgia
597 de = 0x0007, // German
598 de_at = 0x0C07, // German, Austria
599 de_de = 0x0407, // German, Germany
600 de_li = 0x1407, // German, Liechtenstein
601 de_lu = 0x1007, // German, Luxembourg
602 de_ch = 0x0807, // German, Switzerland
603 el = 0x0008, // Greek
604 el_gr = 0x0408, // Greek, Greece
605 kl = 0x006F, // Greenlandic
606 kl_gl = 0x046F, // Greenlandic, Greenland
607 gn = 0x0074, // Guarani
608 gn_py = 0x0474, // Guarani, Paraguay
609 gu = 0x0047, // Gujarati
610 gu_in = 0x0447, // Gujarati, India
611 ha = 0x0068, // Hausa (Latin)
612 ha_latn = 0x7C68, // Hausa (Latin)
613 ha_latn_ng = 0x0468, // Hausa (Latin), Nigeria
614 haw = 0x0075, // Hawaiian
615 haw_us = 0x0475, // Hawaiian, United States
616 he = 0x000D, // Hebrew
617 he_il = 0x040D, // Hebrew, Israel
618 hi = 0x0039, // Hindi
619 hi_in = 0x0439, // Hindi, India
620 hu = 0x000E, // Hungarian
621 hu_hu = 0x040E, // Hungarian, Hungary
622 is = 0x000F, // Icelandic
623 is_is = 0x040F, // Icelandic, Iceland
624 ig = 0x0070, // Igbo
625 ig_ng = 0x0470, // Igbo, Nigeria
626 id = 0x0021, // Indonesian
627 id_id = 0x0421, // Indonesian, Indonesia
628 iu = 0x005D, // Inuktitut (Latin)
629 iu_latn = 0x7C5D, // Inuktitut (Latin)
630 iu_latn_ca = 0x085D, // Inuktitut (Latin), Canada
631 iu_cans = 0x785D, // Inuktitut (Syllabics)
632 iu_cans_ca = 0x045d, // Inuktitut (Syllabics), Canada
633 ga = 0x003C, // Irish
634 ga_ie = 0x083C, // Irish, Ireland
635 it = 0x0010, // Italian
636 it_it = 0x0410, // Italian, Italy
637 it_ch = 0x0810, // Italian, Switzerland
638 ja = 0x0011, // Japanese
639 ja_jp = 0x0411, // Japanese, Japan
640 kn = 0x004B, // Kannada
641 kn_in = 0x044B, // Kannada, India
642 kr_latn_ng = 0x0471, // Kanuri (Latin), Nigeria
643 ks = 0x0060, // Kashmiri
644 ks_arab = 0x0460, // Kashmiri, Perso-Arabic
645 ks_deva_in = 0x0860, // Kashmiri (Devanagari), India
646 kk = 0x003F, // Kazakh
647 kk_kz = 0x043F, // Kazakh, Kazakhstan
648 km = 0x0053, // Khmer
649 km_kh = 0x0453, // Khmer, Cambodia
650 quc = 0x0086, // K'iche
651 quc_latn_gt = 0x0486, // K'iche, Guatemala
652 rw = 0x0087, // Kinyarwanda
653 rw_rw = 0x0487, // Kinyarwanda, Rwanda
654 sw = 0x0041, // Kiswahili
655 sw_ke = 0x0441, // Kiswahili, Kenya
656 kok = 0x0057, // Konkani
657 kok_in = 0x0457, // Konkani, India
658 ko = 0x0012, // Korean
659 ko_kr = 0x0412, // Korean, Korea
660 ky = 0x0040, // Kyrgyz
661 ky_kg = 0x0440, // Kyrgyz, Kyrgyzstan
662 lo = 0x0054, // Lao
663 lo_la = 0x0454, // Lao, Lao P.D.R.
664 la_va = 0x0476, // Latin, Vatican City
665 lv = 0x0026, // Latvian
666 lv_lv = 0x0426, // Latvian, Latvia
667 lt = 0x0027, // Lithuanian
668 lt_lt = 0x0427, // Lithuanian, Lithuania
669 dsb = 0x7C2E, // Lower Sorbian
670 dsb_de = 0x082E, // Lower Sorbian, Germany
671 lb = 0x006E, // Luxembourgish
672 lb_lu = 0x046E, // Luxembourgish, Luxembourg
673 mk = 0x002F, // Macedonian
674 mk_mk = 0x042F, // Macedonian, North Macedonia
675 ms = 0x003E, // Malay
676 ms_bn = 0x083E, // Malay, Brunei Darussalam
677 ms_my = 0x043E, // Malay, Malaysia
678 ml = 0x004C, // Malayalam
679 ml_in = 0x044C, // Malayalam, India
680 mt = 0x003A, // Maltese
681 mt_mt = 0x043A, // Maltese, Malta
682 mi = 0x0081, // Maori
683 mi_nz = 0x0481, // Maori, New Zealand
684 arn = 0x007A, // Mapudungun
685 arn_cl = 0x047A, // Mapudungun, Chile
686 mr = 0x004E, // Marathi
687 mr_in = 0x044E, // Marathi, India
688 moh = 0x007C, // Mohawk
689 moh_ca = 0x047C, // Mohawk, Canada
690 mn = 0x0050, // Mongolian (Cyrillic)
691 mn_cyrl = 0x7850, // Mongolian (Cyrillic)
692 mn_mn = 0x0450, // Mongolian (Cyrillic), Mongolia
693 mn_mong = 0x7C50, // Mongolian (Traditional Mongolian)
694 mn_mong_cn = 0x0850, // Mongolian (Traditional Mongolian), People's Republic of China
695 mn_mong_mn = 0x0C50, // Mongolian (Traditional Mongolian), Mongolia
696 ne = 0x0061, // Nepali
697 ne_in = 0x0861, // Nepali, India
698 ne_np = 0x0461, // Nepali, Nepal
699 no = 0x0014, // Norwegian (Bokmal)
700 nb = 0x7C14, // Norwegian (Bokmal)
701 nb_no = 0x0414, // Norwegian (Bokmal), Norway
702 nn = 0x7814, // Norwegian (Nynorsk)
703 nn_no = 0x0814, // Norwegian (Nynorsk), Norway
704 oc = 0x0082, // Occitan
705 oc_fr = 0x0482, // Occitan, France
706 @"or" = 0x0048, // Odia
707 or_in = 0x0448, // Odia, India
708 om = 0x0072, // Oromo
709 om_et = 0x0472, // Oromo, Ethiopia
710 ps = 0x0063, // Pashto
711 ps_af = 0x0463, // Pashto, Afghanistan
712 fa = 0x0029, // Persian
713 fa_ir = 0x0429, // Persian, Iran
714 pl = 0x0015, // Polish
715 pl_pl = 0x0415, // Polish, Poland
716 pt = 0x0016, // Portuguese
717 pt_br = 0x0416, // Portuguese, Brazil
718 pt_pt = 0x0816, // Portuguese, Portugal
719 qps_ploca = 0x05FE, // Pseudo Language, Pseudo locale for east Asian/complex script localization testing
720 qps_ploc = 0x0501, // Pseudo Language, Pseudo locale used for localization testing
721 qps_plocm = 0x09FF, // Pseudo Language, Pseudo locale used for localization testing of mirrored locales
722 pa = 0x0046, // Punjabi
723 pa_arab = 0x7C46, // Punjabi
724 pa_in = 0x0446, // Punjabi, India
725 pa_arab_pk = 0x0846, // Punjabi, Islamic Republic of Pakistan
726 quz = 0x006B, // Quechua
727 quz_bo = 0x046B, // Quechua, Bolivia
728 quz_ec = 0x086B, // Quechua, Ecuador
729 quz_pe = 0x0C6B, // Quechua, Peru
730 ro = 0x0018, // Romanian
731 ro_md = 0x0818, // Romanian, Moldova
732 ro_ro = 0x0418, // Romanian, Romania
733 rm = 0x0017, // Romansh
734 rm_ch = 0x0417, // Romansh, Switzerland
735 ru = 0x0019, // Russian
736 ru_md = 0x0819, // Russian, Moldova
737 ru_ru = 0x0419, // Russian, Russia
738 sah = 0x0085, // Sakha
739 sah_ru = 0x0485, // Sakha, Russia
740 smn = 0x703B, // Sami (Inari)
741 smn_fi = 0x243B, // Sami (Inari), Finland
742 smj = 0x7C3B, // Sami (Lule)
743 smj_no = 0x103B, // Sami (Lule), Norway
744 smj_se = 0x143B, // Sami (Lule), Sweden
745 se = 0x003B, // Sami (Northern)
746 se_fi = 0x0C3B, // Sami (Northern), Finland
747 se_no = 0x043B, // Sami (Northern), Norway
748 se_se = 0x083B, // Sami (Northern), Sweden
749 sms = 0x743B, // Sami (Skolt)
750 sms_fi = 0x203B, // Sami (Skolt), Finland
751 sma = 0x783B, // Sami (Southern)
752 sma_no = 0x183B, // Sami (Southern), Norway
753 sma_se = 0x1C3B, // Sami (Southern), Sweden
754 sa = 0x004F, // Sanskrit
755 sa_in = 0x044F, // Sanskrit, India
756 gd = 0x0091, // Scottish Gaelic
757 gd_gb = 0x0491, // Scottish Gaelic, United Kingdom
758 sr_cyrl = 0x6C1A, // Serbian (Cyrillic)
759 sr_cyrl_ba = 0x1C1A, // Serbian (Cyrillic), Bosnia and Herzegovina
760 sr_cyrl_me = 0x301A, // Serbian (Cyrillic), Montenegro
761 sr_cyrl_rs = 0x281A, // Serbian (Cyrillic), Serbia
762 sr_cyrl_cs = 0x0C1A, // Serbian (Cyrillic), Serbia and Montenegro (Former)
763 sr_latn = 0x701A, // Serbian (Latin)
764 sr = 0x7C1A, // Serbian (Latin)
765 sr_latn_ba = 0x181A, // Serbian (Latin), Bosnia and Herzegovina
766 sr_latn_me = 0x2c1A, // Serbian (Latin), Montenegro
767 sr_latn_rs = 0x241A, // Serbian (Latin), Serbia
768 sr_latn_cs = 0x081A, // Serbian (Latin), Serbia and Montenegro (Former)
769 nso = 0x006C, // Sesotho sa Leboa
770 nso_za = 0x046C, // Sesotho sa Leboa, South Africa
771 tn = 0x0032, // Setswana
772 tn_bw = 0x0832, // Setswana, Botswana
773 tn_za = 0x0432, // Setswana, South Africa
774 sd = 0x0059, // Sindhi
775 sd_arab = 0x7C59, // Sindhi
776 sd_arab_pk = 0x0859, // Sindhi, Islamic Republic of Pakistan
777 si = 0x005B, // Sinhala
778 si_lk = 0x045B, // Sinhala, Sri Lanka
779 sk = 0x001B, // Slovak
780 sk_sk = 0x041B, // Slovak, Slovakia
781 sl = 0x0024, // Slovenian
782 sl_si = 0x0424, // Slovenian, Slovenia
783 so = 0x0077, // Somali
784 so_so = 0x0477, // Somali, Somalia
785 st = 0x0030, // Sotho
786 st_za = 0x0430, // Sotho, South Africa
787 es = 0x000A, // Spanish
788 es_ar = 0x2C0A, // Spanish, Argentina
789 es_ve = 0x200A, // Spanish, Bolivarian Republic of Venezuela
790 es_bo = 0x400A, // Spanish, Bolivia
791 es_cl = 0x340A, // Spanish, Chile
792 es_co = 0x240A, // Spanish, Colombia
793 es_cr = 0x140A, // Spanish, Costa Rica
794 es_cu = 0x5c0A, // Spanish, Cuba
795 es_do = 0x1c0A, // Spanish, Dominican Republic
796 es_ec = 0x300A, // Spanish, Ecuador
797 es_sv = 0x440A, // Spanish, El Salvador
798 es_gt = 0x100A, // Spanish, Guatemala
799 es_hn = 0x480A, // Spanish, Honduras
800 es_419 = 0x580A, // Spanish, Latin America
801 es_mx = 0x080A, // Spanish, Mexico
802 es_ni = 0x4C0A, // Spanish, Nicaragua
803 es_pa = 0x180A, // Spanish, Panama
804 es_py = 0x3C0A, // Spanish, Paraguay
805 es_pe = 0x280A, // Spanish, Peru
806 es_pr = 0x500A, // Spanish, Puerto Rico
807 es_es_tradnl = 0x040A, // Spanish, Spain
808 es_es = 0x0c0A, // Spanish, Spain
809 es_us = 0x540A, // Spanish, United States
810 es_uy = 0x380A, // Spanish, Uruguay
811 sv = 0x001D, // Swedish
812 sv_fi = 0x081D, // Swedish, Finland
813 sv_se = 0x041D, // Swedish, Sweden
814 syr = 0x005A, // Syriac
815 syr_sy = 0x045A, // Syriac, Syria
816 tg = 0x0028, // Tajik (Cyrillic)
817 tg_cyrl = 0x7C28, // Tajik (Cyrillic)
818 tg_cyrl_tj = 0x0428, // Tajik (Cyrillic), Tajikistan
819 tzm = 0x005F, // Tamazight (Latin)
820 tzm_latn = 0x7C5F, // Tamazight (Latin)
821 tzm_latn_dz = 0x085F, // Tamazight (Latin), Algeria
822 ta = 0x0049, // Tamil
823 ta_in = 0x0449, // Tamil, India
824 ta_lk = 0x0849, // Tamil, Sri Lanka
825 tt = 0x0044, // Tatar
826 tt_ru = 0x0444, // Tatar, Russia
827 te = 0x004A, // Telugu
828 te_in = 0x044A, // Telugu, India
829 th = 0x001E, // Thai
830 th_th = 0x041E, // Thai, Thailand
831 bo = 0x0051, // Tibetan
832 bo_cn = 0x0451, // Tibetan, People's Republic of China
833 ti = 0x0073, // Tigrinya
834 ti_er = 0x0873, // Tigrinya, Eritrea
835 ti_et = 0x0473, // Tigrinya, Ethiopia
836 ts = 0x0031, // Tsonga
837 ts_za = 0x0431, // Tsonga, South Africa
838 tr = 0x001F, // Turkish
839 tr_tr = 0x041F, // Turkish, Turkey
840 tk = 0x0042, // Turkmen
841 tk_tm = 0x0442, // Turkmen, Turkmenistan
842 uk = 0x0022, // Ukrainian
843 uk_ua = 0x0422, // Ukrainian, Ukraine
844 hsb = 0x002E, // Upper Sorbian
845 hsb_de = 0x042E, // Upper Sorbian, Germany
846 ur = 0x0020, // Urdu
847 ur_in = 0x0820, // Urdu, India
848 ur_pk = 0x0420, // Urdu, Islamic Republic of Pakistan
849 ug = 0x0080, // Uyghur
850 ug_cn = 0x0480, // Uyghur, People's Republic of China
851 uz_cyrl = 0x7843, // Uzbek (Cyrillic)
852 uz_cyrl_uz = 0x0843, // Uzbek (Cyrillic), Uzbekistan
853 uz = 0x0043, // Uzbek (Latin)
854 uz_latn = 0x7C43, // Uzbek (Latin)
855 uz_latn_uz = 0x0443, // Uzbek (Latin), Uzbekistan
856 ca_es_valencia = 0x0803, // Valencian, Spain
857 ve = 0x0033, // Venda
858 ve_za = 0x0433, // Venda, South Africa
859 vi = 0x002A, // Vietnamese
860 vi_vn = 0x042A, // Vietnamese, Vietnam
861 cy = 0x0052, // Welsh
862 cy_gb = 0x0452, // Welsh, United Kingdom
863 wo = 0x0088, // Wolof
864 wo_sn = 0x0488, // Wolof, Senegal
865 xh = 0x0034, // Xhosa
866 xh_za = 0x0434, // Xhosa, South Africa
867 ii = 0x0078, // Yi
868 ii_cn = 0x0478, // Yi, People's Republic of China
869 yi_001 = 0x043D, // Yiddish, World
870 yo = 0x006A, // Yoruba
871 yo_ng = 0x046A, // Yoruba, Nigeria
872 zu = 0x0035, // Zulu
873 zu_za = 0x0435, // Zulu, South Africa
874
875 /// Special case
876 x_iv_mathan = 0x007F, // LANG_INVARIANT, "math alphanumeric sorting"
877};
src/resinator/lex.zig deleted-1098
...@@ -1,1098 +0,0 @@
1//! Expects to be run after the C preprocessor and after `removeComments`.
2//! This means that the lexer assumes that:
3//! - Splices ('\' at the end of a line) have been handled/collapsed.
4//! - Preprocessor directives and macros have been expanded (any remaining should be skipped with the exception of `#pragma code_page`).
5//! - All comments have been removed.
6
7const std = @import("std");
8const ErrorDetails = @import("errors.zig").ErrorDetails;
9const columnWidth = @import("literals.zig").columnWidth;
10const code_pages = @import("code_pages.zig");
11const CodePage = code_pages.CodePage;
12const SourceMappings = @import("source_mapping.zig").SourceMappings;
13const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
14
15const dumpTokensDuringTests = false;
16
17pub const default_max_string_literal_codepoints = 4097;
18
19pub const Token = struct {
20 id: Id,
21 start: usize,
22 end: usize,
23 line_number: usize,
24
25 pub const Id = enum {
26 literal,
27 number,
28 quoted_ascii_string,
29 quoted_wide_string,
30 operator,
31 begin,
32 end,
33 comma,
34 open_paren,
35 close_paren,
36 /// This Id is only used for errors, the Lexer will never return one
37 /// of these from a `next` call.
38 preprocessor_command,
39 invalid,
40 eof,
41
42 pub fn nameForErrorDisplay(self: Id) []const u8 {
43 return switch (self) {
44 .literal => "<literal>",
45 .number => "<number>",
46 .quoted_ascii_string => "<quoted ascii string>",
47 .quoted_wide_string => "<quoted wide string>",
48 .operator => "<operator>",
49 .begin => "<'{' or BEGIN>",
50 .end => "<'}' or END>",
51 .comma => ",",
52 .open_paren => "(",
53 .close_paren => ")",
54 .preprocessor_command => "<preprocessor command>",
55 .invalid => unreachable,
56 .eof => "<eof>",
57 };
58 }
59 };
60
61 pub fn slice(self: Token, buffer: []const u8) []const u8 {
62 return buffer[self.start..self.end];
63 }
64
65 pub fn nameForErrorDisplay(self: Token, buffer: []const u8) []const u8 {
66 return switch (self.id) {
67 .eof => self.id.nameForErrorDisplay(),
68 else => self.slice(buffer),
69 };
70 }
71
72 /// Returns 0-based column
73 pub fn calculateColumn(token: Token, source: []const u8, tab_columns: usize, maybe_line_start: ?usize) usize {
74 const line_start = maybe_line_start orelse token.getLineStart(source);
75
76 var i: usize = line_start;
77 var column: usize = 0;
78 while (i < token.start) : (i += 1) {
79 column += columnWidth(column, source[i], tab_columns);
80 }
81 return column;
82 }
83
84 // TODO: This doesn't necessarily match up with how we count line numbers, but where a line starts
85 // has a knock-on effect on calculateColumn. More testing is needed to determine what needs
86 // to be changed to make this both (1) match how line numbers are counted and (2) match how
87 // the Win32 RC compiler counts tab columns.
88 //
89 // (the TODO in currentIndexFormsLineEndingPair should be taken into account as well)
90 pub fn getLineStart(token: Token, source: []const u8) usize {
91 const line_start = line_start: {
92 if (token.start != 0) {
93 // start checking at the byte before the token
94 var index = token.start - 1;
95 while (true) {
96 if (source[index] == '\n') break :line_start @min(source.len - 1, index + 1);
97 if (index != 0) index -= 1 else break;
98 }
99 }
100 break :line_start 0;
101 };
102 return line_start;
103 }
104
105 pub fn getLine(token: Token, source: []const u8, maybe_line_start: ?usize) []const u8 {
106 const line_start = maybe_line_start orelse token.getLineStart(source);
107
108 var line_end = line_start + 1;
109 if (line_end >= source.len or source[line_end] == '\n') return source[line_start..line_start];
110 while (line_end < source.len and source[line_end] != '\n') : (line_end += 1) {}
111 while (line_end > 0 and source[line_end - 1] == '\r') : (line_end -= 1) {}
112
113 return source[line_start..line_end];
114 }
115
116 pub fn isStringLiteral(token: Token) bool {
117 return token.id == .quoted_ascii_string or token.id == .quoted_wide_string;
118 }
119};
120
121pub const LineHandler = struct {
122 line_number: usize = 1,
123 buffer: []const u8,
124 last_line_ending_index: ?usize = null,
125
126 /// Like incrementLineNumber but checks that the current char is a line ending first.
127 /// Returns the new line number if it was incremented, null otherwise.
128 pub fn maybeIncrementLineNumber(self: *LineHandler, cur_index: usize) ?usize {
129 const c = self.buffer[cur_index];
130 if (c == '\r' or c == '\n') {
131 return self.incrementLineNumber(cur_index);
132 }
133 return null;
134 }
135
136 /// Increments line_number appropriately (handling line ending pairs)
137 /// and returns the new line number if it was incremented, or null otherwise.
138 pub fn incrementLineNumber(self: *LineHandler, cur_index: usize) ?usize {
139 if (self.currentIndexFormsLineEndingPair(cur_index)) {
140 self.last_line_ending_index = null;
141 return null;
142 } else {
143 self.line_number += 1;
144 self.last_line_ending_index = cur_index;
145 return self.line_number;
146 }
147 }
148
149 /// \r\n and \n\r pairs are treated as a single line ending (but not \r\r \n\n)
150 /// expects self.index and last_line_ending_index (if non-null) to contain line endings
151 ///
152 /// TODO: This is not really how the Win32 RC compiler handles line endings. Instead, it
153 /// seems to drop all carriage returns during preprocessing and then replace all
154 /// remaining line endings with well-formed CRLF pairs (e.g. `<CR>a<CR>b<LF>c` becomes `ab<CR><LF>c`).
155 /// Handling this the same as the Win32 RC compiler would need control over the preprocessor,
156 /// since Clang converts unpaired <CR> into unpaired <LF>.
157 pub fn currentIndexFormsLineEndingPair(self: *const LineHandler, cur_index: usize) bool {
158 if (self.last_line_ending_index == null) return false;
159
160 // must immediately precede the current index, we know cur_index must
161 // be >= 1 since last_line_ending_index is non-null (so if the subtraction
162 // overflows it is a bug at the callsite of this function).
163 if (self.last_line_ending_index.? != cur_index - 1) return false;
164
165 const cur_line_ending = self.buffer[cur_index];
166 const last_line_ending = self.buffer[self.last_line_ending_index.?];
167
168 // sanity check
169 std.debug.assert(cur_line_ending == '\r' or cur_line_ending == '\n');
170 std.debug.assert(last_line_ending == '\r' or last_line_ending == '\n');
171
172 // can't be \n\n or \r\r
173 if (last_line_ending == cur_line_ending) return false;
174
175 return true;
176 }
177};
178
179pub const LexError = error{
180 UnfinishedStringLiteral,
181 StringLiteralTooLong,
182 InvalidNumberWithExponent,
183 InvalidDigitCharacterInNumberLiteral,
184 IllegalByte,
185 IllegalByteOutsideStringLiterals,
186 IllegalCodepointOutsideStringLiterals,
187 IllegalByteOrderMark,
188 IllegalPrivateUseCharacter,
189 FoundCStyleEscapedQuote,
190 CodePagePragmaMissingLeftParen,
191 CodePagePragmaMissingRightParen,
192 /// Can be caught and ignored
193 CodePagePragmaInvalidCodePage,
194 CodePagePragmaNotInteger,
195 CodePagePragmaOverflow,
196 CodePagePragmaUnsupportedCodePage,
197 /// Can be caught and ignored
198 CodePagePragmaInIncludedFile,
199};
200
201pub const Lexer = struct {
202 const Self = @This();
203
204 buffer: []const u8,
205 index: usize,
206 line_handler: LineHandler,
207 at_start_of_line: bool = true,
208 error_context_token: ?Token = null,
209 current_code_page: CodePage,
210 default_code_page: CodePage,
211 source_mappings: ?*SourceMappings,
212 max_string_literal_codepoints: u15,
213 /// Needed to determine whether or not the output code page should
214 /// be set in the parser.
215 seen_pragma_code_pages: u2 = 0,
216
217 pub const Error = LexError;
218
219 pub const LexerOptions = struct {
220 default_code_page: CodePage = .windows1252,
221 source_mappings: ?*SourceMappings = null,
222 max_string_literal_codepoints: u15 = default_max_string_literal_codepoints,
223 };
224
225 pub fn init(buffer: []const u8, options: LexerOptions) Self {
226 return Self{
227 .buffer = buffer,
228 .index = 0,
229 .current_code_page = options.default_code_page,
230 .default_code_page = options.default_code_page,
231 .source_mappings = options.source_mappings,
232 .max_string_literal_codepoints = options.max_string_literal_codepoints,
233 .line_handler = .{ .buffer = buffer },
234 };
235 }
236
237 pub fn dump(self: *Self, token: *const Token) void {
238 std.debug.print("{s}:{d}: {s}\n", .{ @tagName(token.id), token.line_number, std.fmt.fmtSliceEscapeLower(token.slice(self.buffer)) });
239 }
240
241 pub const LexMethod = enum {
242 whitespace_delimiter_only,
243 normal,
244 normal_expect_operator,
245 };
246
247 pub fn next(self: *Self, comptime method: LexMethod) LexError!Token {
248 switch (method) {
249 .whitespace_delimiter_only => return self.nextWhitespaceDelimeterOnly(),
250 .normal => return self.nextNormal(),
251 .normal_expect_operator => return self.nextNormalWithContext(.expect_operator),
252 }
253 }
254
255 const StateWhitespaceDelimiterOnly = enum {
256 start,
257 literal,
258 preprocessor,
259 semicolon,
260 };
261
262 pub fn nextWhitespaceDelimeterOnly(self: *Self) LexError!Token {
263 const start_index = self.index;
264 var result = Token{
265 .id = .eof,
266 .start = start_index,
267 .end = undefined,
268 .line_number = self.line_handler.line_number,
269 };
270 var state = StateWhitespaceDelimiterOnly.start;
271
272 while (self.current_code_page.codepointAt(self.index, self.buffer)) |codepoint| : (self.index += codepoint.byte_len) {
273 const c = codepoint.value;
274 try self.checkForIllegalCodepoint(codepoint, false);
275 switch (state) {
276 .start => switch (c) {
277 '\r', '\n' => {
278 result.start = self.index + 1;
279 result.line_number = self.incrementLineNumber();
280 },
281 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
282 result.start = self.index + 1;
283 },
284 // NBSP only counts as whitespace at the start of a line (but
285 // can be intermixed with other whitespace). Who knows why.
286 '\xA0' => if (self.at_start_of_line) {
287 result.start = self.index + codepoint.byte_len;
288 } else {
289 state = .literal;
290 self.at_start_of_line = false;
291 },
292 '#' => {
293 if (self.at_start_of_line) {
294 state = .preprocessor;
295 } else {
296 state = .literal;
297 }
298 self.at_start_of_line = false;
299 },
300 // Semi-colon acts as a line-terminator, but in this lexing mode
301 // that's only true if it's at the start of a line.
302 ';' => {
303 if (self.at_start_of_line) {
304 state = .semicolon;
305 }
306 self.at_start_of_line = false;
307 },
308 else => {
309 state = .literal;
310 self.at_start_of_line = false;
311 },
312 },
313 .literal => switch (c) {
314 '\r', '\n', ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
315 result.id = .literal;
316 break;
317 },
318 else => {},
319 },
320 .preprocessor => switch (c) {
321 '\r', '\n' => {
322 try self.evaluatePreprocessorCommand(result.start, self.index);
323 result.start = self.index + 1;
324 state = .start;
325 result.line_number = self.incrementLineNumber();
326 },
327 else => {},
328 },
329 .semicolon => switch (c) {
330 '\r', '\n' => {
331 result.start = self.index + 1;
332 state = .start;
333 result.line_number = self.incrementLineNumber();
334 },
335 else => {},
336 },
337 }
338 } else { // got EOF
339 switch (state) {
340 .start, .semicolon => {},
341 .literal => {
342 result.id = .literal;
343 },
344 .preprocessor => {
345 try self.evaluatePreprocessorCommand(result.start, self.index);
346 result.start = self.index;
347 },
348 }
349 }
350
351 result.end = self.index;
352 return result;
353 }
354
355 const StateNormal = enum {
356 start,
357 literal_or_quoted_wide_string,
358 quoted_ascii_string,
359 quoted_wide_string,
360 quoted_ascii_string_escape,
361 quoted_wide_string_escape,
362 quoted_ascii_string_maybe_end,
363 quoted_wide_string_maybe_end,
364 literal,
365 number_literal,
366 preprocessor,
367 semicolon,
368 // end
369 e,
370 en,
371 // begin
372 b,
373 be,
374 beg,
375 begi,
376 };
377
378 /// TODO: A not-terrible name
379 pub fn nextNormal(self: *Self) LexError!Token {
380 return self.nextNormalWithContext(.any);
381 }
382
383 pub fn nextNormalWithContext(self: *Self, context: enum { expect_operator, any }) LexError!Token {
384 const start_index = self.index;
385 var result = Token{
386 .id = .eof,
387 .start = start_index,
388 .end = undefined,
389 .line_number = self.line_handler.line_number,
390 };
391 var state = StateNormal.start;
392
393 // Note: The Windows RC compiler uses a non-standard method of computing
394 // length for its 'string literal too long' errors; it isn't easily
395 // explained or intuitive (it's sort-of pre-parsed byte length but with
396 // a few of exceptions/edge cases).
397 //
398 // It also behaves strangely with non-ASCII codepoints, e.g. even though the default
399 // limit is 4097, you can only have 4094 € codepoints (1 UTF-16 code unit each),
400 // and 2048 𐐷 codepoints (2 UTF-16 code units each).
401 //
402 // TODO: Understand this more, bring it more in line with how the Win32 limits work.
403 // Alternatively, do something that makes more sense but may be more permissive.
404 var string_literal_length: usize = 0;
405 // Keeping track of the string literal column prevents pathological edge cases when
406 // there are tons of tab stop characters within a string literal.
407 var string_literal_column: usize = 0;
408 var string_literal_collapsing_whitespace: bool = false;
409 var still_could_have_exponent: bool = true;
410 var exponent_index: ?usize = null;
411 while (self.current_code_page.codepointAt(self.index, self.buffer)) |codepoint| : (self.index += codepoint.byte_len) {
412 const c = codepoint.value;
413 const in_string_literal = switch (state) {
414 .quoted_ascii_string,
415 .quoted_wide_string,
416 .quoted_ascii_string_escape,
417 .quoted_wide_string_escape,
418 .quoted_ascii_string_maybe_end,
419 .quoted_wide_string_maybe_end,
420 =>
421 // If the current line is not the same line as the start of the string literal,
422 // then we want to treat the current codepoint as 'not in a string literal'
423 // for the purposes of detecting illegal codepoints. This means that we will
424 // error on illegal-outside-string-literal characters that are outside string
425 // literals from the perspective of a C preprocessor, but that may be
426 // inside string literals from the perspective of the RC lexer. For example,
427 // "hello
428 // @"
429 // will be treated as a single string literal by the RC lexer but the Win32
430 // preprocessor will consider this an unclosed string literal followed by
431 // the character @ and ", and will therefore error since the Win32 RC preprocessor
432 // errors on the @ character outside string literals.
433 //
434 // By doing this here, we can effectively emulate the Win32 RC preprocessor behavior
435 // at lex-time, and avoid the need for a separate step that checks for this edge-case
436 // specifically.
437 result.line_number == self.line_handler.line_number,
438 else => false,
439 };
440 try self.checkForIllegalCodepoint(codepoint, in_string_literal);
441 switch (state) {
442 .start => switch (c) {
443 '\r', '\n' => {
444 result.start = self.index + 1;
445 result.line_number = self.incrementLineNumber();
446 },
447 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F' => {
448 result.start = self.index + 1;
449 },
450 // NBSP only counts as whitespace at the start of a line (but
451 // can be intermixed with other whitespace). Who knows why.
452 '\xA0' => if (self.at_start_of_line) {
453 result.start = self.index + codepoint.byte_len;
454 } else {
455 state = .literal;
456 self.at_start_of_line = false;
457 },
458 'L', 'l' => {
459 state = .literal_or_quoted_wide_string;
460 self.at_start_of_line = false;
461 },
462 'E', 'e' => {
463 state = .e;
464 self.at_start_of_line = false;
465 },
466 'B', 'b' => {
467 state = .b;
468 self.at_start_of_line = false;
469 },
470 '"' => {
471 state = .quoted_ascii_string;
472 self.at_start_of_line = false;
473 string_literal_collapsing_whitespace = false;
474 string_literal_length = 0;
475
476 var dummy_token = Token{
477 .start = self.index,
478 .end = self.index,
479 .line_number = self.line_handler.line_number,
480 .id = .invalid,
481 };
482 string_literal_column = dummy_token.calculateColumn(self.buffer, 8, null);
483 },
484 '+', '&', '|' => {
485 self.index += 1;
486 result.id = .operator;
487 self.at_start_of_line = false;
488 break;
489 },
490 '-' => {
491 if (context == .expect_operator) {
492 self.index += 1;
493 result.id = .operator;
494 self.at_start_of_line = false;
495 break;
496 } else {
497 state = .number_literal;
498 still_could_have_exponent = true;
499 exponent_index = null;
500 self.at_start_of_line = false;
501 }
502 },
503 '0'...'9', '~' => {
504 state = .number_literal;
505 still_could_have_exponent = true;
506 exponent_index = null;
507 self.at_start_of_line = false;
508 },
509 '#' => {
510 if (self.at_start_of_line) {
511 state = .preprocessor;
512 } else {
513 state = .literal;
514 }
515 self.at_start_of_line = false;
516 },
517 ';' => {
518 state = .semicolon;
519 self.at_start_of_line = false;
520 },
521 '{', '}' => {
522 self.index += 1;
523 result.id = if (c == '{') .begin else .end;
524 self.at_start_of_line = false;
525 break;
526 },
527 '(', ')' => {
528 self.index += 1;
529 result.id = if (c == '(') .open_paren else .close_paren;
530 self.at_start_of_line = false;
531 break;
532 },
533 ',' => {
534 self.index += 1;
535 result.id = .comma;
536 self.at_start_of_line = false;
537 break;
538 },
539 else => {
540 if (isNonAsciiDigit(c)) {
541 self.error_context_token = .{
542 .id = .number,
543 .start = result.start,
544 .end = self.index + 1,
545 .line_number = self.line_handler.line_number,
546 };
547 return error.InvalidDigitCharacterInNumberLiteral;
548 }
549 state = .literal;
550 self.at_start_of_line = false;
551 },
552 },
553 .preprocessor => switch (c) {
554 '\r', '\n' => {
555 try self.evaluatePreprocessorCommand(result.start, self.index);
556 result.start = self.index + 1;
557 state = .start;
558 result.line_number = self.incrementLineNumber();
559 },
560 else => {},
561 },
562 // Semi-colon acts as a line-terminator--everything is skipped until
563 // the next line.
564 .semicolon => switch (c) {
565 '\r', '\n' => {
566 result.start = self.index + 1;
567 state = .start;
568 result.line_number = self.incrementLineNumber();
569 },
570 else => {},
571 },
572 .number_literal => switch (c) {
573 // zig fmt: off
574 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
575 '\r', '\n', '"', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
576 '\'', ';', '=',
577 => {
578 // zig fmt: on
579 result.id = .number;
580 break;
581 },
582 '0'...'9' => {
583 if (exponent_index) |exp_i| {
584 if (self.index - 1 == exp_i) {
585 // Note: This being an error is a quirk of the preprocessor used by
586 // the Win32 RC compiler.
587 self.error_context_token = .{
588 .id = .number,
589 .start = result.start,
590 .end = self.index + 1,
591 .line_number = self.line_handler.line_number,
592 };
593 return error.InvalidNumberWithExponent;
594 }
595 }
596 },
597 'e', 'E' => {
598 if (still_could_have_exponent) {
599 exponent_index = self.index;
600 still_could_have_exponent = false;
601 }
602 },
603 else => {
604 if (isNonAsciiDigit(c)) {
605 self.error_context_token = .{
606 .id = .number,
607 .start = result.start,
608 .end = self.index + 1,
609 .line_number = self.line_handler.line_number,
610 };
611 return error.InvalidDigitCharacterInNumberLiteral;
612 }
613 still_could_have_exponent = false;
614 },
615 },
616 .literal_or_quoted_wide_string => switch (c) {
617 // zig fmt: off
618 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
619 '\r', '\n', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
620 '\'', ';', '=',
621 // zig fmt: on
622 => {
623 result.id = .literal;
624 break;
625 },
626 '"' => {
627 state = .quoted_wide_string;
628 string_literal_collapsing_whitespace = false;
629 string_literal_length = 0;
630
631 var dummy_token = Token{
632 .start = self.index,
633 .end = self.index,
634 .line_number = self.line_handler.line_number,
635 .id = .invalid,
636 };
637 string_literal_column = dummy_token.calculateColumn(self.buffer, 8, null);
638 },
639 else => {
640 state = .literal;
641 },
642 },
643 .literal => switch (c) {
644 // zig fmt: off
645 ' ', '\t', '\x05'...'\x08', '\x0B'...'\x0C', '\x0E'...'\x1F',
646 '\r', '\n', '"', ',', '{', '}', '+', '-', '|', '&', '~', '(', ')',
647 '\'', ';', '=',
648 => {
649 // zig fmt: on
650 result.id = .literal;
651 break;
652 },
653 else => {},
654 },
655 .e => switch (c) {
656 'N', 'n' => {
657 state = .en;
658 },
659 else => {
660 state = .literal;
661 self.index -= 1;
662 },
663 },
664 .en => switch (c) {
665 'D', 'd' => {
666 result.id = .end;
667 self.index += 1;
668 break;
669 },
670 else => {
671 state = .literal;
672 self.index -= 1;
673 },
674 },
675 .b => switch (c) {
676 'E', 'e' => {
677 state = .be;
678 },
679 else => {
680 state = .literal;
681 self.index -= 1;
682 },
683 },
684 .be => switch (c) {
685 'G', 'g' => {
686 state = .beg;
687 },
688 else => {
689 state = .literal;
690 self.index -= 1;
691 },
692 },
693 .beg => switch (c) {
694 'I', 'i' => {
695 state = .begi;
696 },
697 else => {
698 state = .literal;
699 self.index -= 1;
700 },
701 },
702 .begi => switch (c) {
703 'N', 'n' => {
704 result.id = .begin;
705 self.index += 1;
706 break;
707 },
708 else => {
709 state = .literal;
710 self.index -= 1;
711 },
712 },
713 .quoted_ascii_string, .quoted_wide_string => switch (c) {
714 '"' => {
715 string_literal_column += 1;
716 state = if (state == .quoted_ascii_string) .quoted_ascii_string_maybe_end else .quoted_wide_string_maybe_end;
717 },
718 '\\' => {
719 string_literal_length += 1;
720 string_literal_column += 1;
721 state = if (state == .quoted_ascii_string) .quoted_ascii_string_escape else .quoted_wide_string_escape;
722 },
723 '\r' => {
724 string_literal_column = 0;
725 // \r doesn't count towards string literal length
726
727 // Increment line number but don't affect the result token's line number
728 _ = self.incrementLineNumber();
729 },
730 '\n' => {
731 string_literal_column = 0;
732 // first \n expands to <space><\n>
733 if (!string_literal_collapsing_whitespace) {
734 string_literal_length += 2;
735 string_literal_collapsing_whitespace = true;
736 }
737 // the rest are collapsed into the <space><\n>
738
739 // Increment line number but don't affect the result token's line number
740 _ = self.incrementLineNumber();
741 },
742 // only \t, space, Vertical Tab, and Form Feed count as whitespace when collapsing
743 '\t', ' ', '\x0b', '\x0c' => {
744 if (!string_literal_collapsing_whitespace) {
745 // Literal tab characters are counted as the number of space characters
746 // needed to reach the next 8-column tab stop.
747 const width = columnWidth(string_literal_column, @intCast(c), 8);
748 string_literal_length += width;
749 string_literal_column += width;
750 }
751 },
752 else => {
753 string_literal_collapsing_whitespace = false;
754 string_literal_length += 1;
755 string_literal_column += 1;
756 },
757 },
758 .quoted_ascii_string_escape, .quoted_wide_string_escape => switch (c) {
759 '"' => {
760 self.error_context_token = .{
761 .id = .invalid,
762 .start = self.index - 1,
763 .end = self.index + 1,
764 .line_number = self.line_handler.line_number,
765 };
766 return error.FoundCStyleEscapedQuote;
767 },
768 else => {
769 string_literal_length += 1;
770 string_literal_column += 1;
771 state = if (state == .quoted_ascii_string_escape) .quoted_ascii_string else .quoted_wide_string;
772 },
773 },
774 .quoted_ascii_string_maybe_end, .quoted_wide_string_maybe_end => switch (c) {
775 '"' => {
776 state = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
777 // Escaped quotes count as 1 char for string literal length checks.
778 // Since we did not increment on the first " (because it could have been
779 // the end of the quoted string), we increment here
780 string_literal_length += 1;
781 string_literal_column += 1;
782 },
783 else => {
784 result.id = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
785 break;
786 },
787 },
788 }
789 } else { // got EOF
790 switch (state) {
791 .start, .semicolon => {},
792 .literal_or_quoted_wide_string, .literal, .e, .en, .b, .be, .beg, .begi => {
793 result.id = .literal;
794 },
795 .preprocessor => {
796 try self.evaluatePreprocessorCommand(result.start, self.index);
797 result.start = self.index;
798 },
799 .number_literal => {
800 result.id = .number;
801 },
802 .quoted_ascii_string_maybe_end, .quoted_wide_string_maybe_end => {
803 result.id = if (state == .quoted_ascii_string_maybe_end) .quoted_ascii_string else .quoted_wide_string;
804 },
805 .quoted_ascii_string,
806 .quoted_wide_string,
807 .quoted_ascii_string_escape,
808 .quoted_wide_string_escape,
809 => {
810 self.error_context_token = .{
811 .id = .eof,
812 .start = self.index,
813 .end = self.index,
814 .line_number = self.line_handler.line_number,
815 };
816 return LexError.UnfinishedStringLiteral;
817 },
818 }
819 }
820
821 result.end = self.index;
822
823 if (result.id == .quoted_ascii_string or result.id == .quoted_wide_string) {
824 if (string_literal_length > self.max_string_literal_codepoints) {
825 self.error_context_token = result;
826 return LexError.StringLiteralTooLong;
827 }
828 }
829
830 return result;
831 }
832
833 /// Increments line_number appropriately (handling line ending pairs)
834 /// and returns the new line number.
835 fn incrementLineNumber(self: *Self) usize {
836 _ = self.line_handler.incrementLineNumber(self.index);
837 self.at_start_of_line = true;
838 return self.line_handler.line_number;
839 }
840
841 fn checkForIllegalCodepoint(self: *Self, codepoint: code_pages.Codepoint, in_string_literal: bool) LexError!void {
842 const err = switch (codepoint.value) {
843 // 0x00 = NUL
844 // 0x1A = Substitute (treated as EOF)
845 // NOTE: 0x1A gets treated as EOF by the clang preprocessor so after a .rc file
846 // is run through the clang preprocessor it will no longer have 0x1A characters in it.
847 // 0x7F = DEL (treated as a context-specific terminator by the Windows RC compiler)
848 0x00, 0x1A, 0x7F => error.IllegalByte,
849 // 0x01...0x03 result in strange 'macro definition too big' errors when used outside of string literals
850 // 0x04 is valid but behaves strangely (sort of acts as a 'skip the next character' instruction)
851 0x01...0x04 => if (!in_string_literal) error.IllegalByteOutsideStringLiterals else return,
852 // @ and ` both result in error RC2018: unknown character '0x60' (and subsequently
853 // fatal error RC1116: RC terminating after preprocessor errors) if they are ever used
854 // outside of string literals. Not exactly sure why this would be the case, though.
855 // TODO: Make sure there aren't any exceptions
856 '@', '`' => if (!in_string_literal) error.IllegalByteOutsideStringLiterals else return,
857 // The Byte Order Mark is mostly skipped over by the Windows RC compiler, but
858 // there are edge cases where it leads to cryptic 'compiler limit : macro definition too big'
859 // errors (e.g. a BOM within a number literal). By making this illegal we avoid having to
860 // deal with a lot of edge cases and remove the potential footgun of the bytes of a BOM
861 // being 'missing' when included in a string literal (the Windows RC compiler acts as
862 // if the codepoint was never part of the string literal).
863 '\u{FEFF}' => error.IllegalByteOrderMark,
864 // Similar deal with this private use codepoint, it gets skipped/ignored by the
865 // RC compiler (but without the cryptic errors). Silently dropping bytes still seems like
866 // enough of a footgun with no real use-cases that it's still worth erroring instead of
867 // emulating the RC compiler's behavior, though.
868 '\u{E000}' => error.IllegalPrivateUseCharacter,
869 // These codepoints lead to strange errors when used outside of string literals,
870 // and miscompilations when used within string literals. We avoid the miscompilation
871 // within string literals and emit a warning, but outside of string literals it makes
872 // more sense to just disallow these codepoints.
873 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => if (!in_string_literal) error.IllegalCodepointOutsideStringLiterals else return,
874 else => return,
875 };
876 self.error_context_token = .{
877 .id = .invalid,
878 .start = self.index,
879 .end = self.index + codepoint.byte_len,
880 .line_number = self.line_handler.line_number,
881 };
882 return err;
883 }
884
885 fn evaluatePreprocessorCommand(self: *Self, start: usize, end: usize) !void {
886 const token = Token{
887 .id = .preprocessor_command,
888 .start = start,
889 .end = end,
890 .line_number = self.line_handler.line_number,
891 };
892 errdefer self.error_context_token = token;
893 const full_command = self.buffer[start..end];
894 var command = full_command;
895
896 // Anything besides exactly this is ignored by the Windows RC implementation
897 const expected_directive = "#pragma";
898 if (!std.mem.startsWith(u8, command, expected_directive)) return;
899 command = command[expected_directive.len..];
900
901 if (command.len == 0 or !std.ascii.isWhitespace(command[0])) return;
902 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
903 command = command[1..];
904 }
905
906 // Note: CoDe_PaGeZ is also treated as "code_page" by the Windows RC implementation,
907 // and it will error with 'Missing left parenthesis in code_page #pragma'
908 const expected_extension = "code_page";
909 if (!std.ascii.startsWithIgnoreCase(command, expected_extension)) return;
910 command = command[expected_extension.len..];
911
912 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
913 command = command[1..];
914 }
915
916 if (command.len == 0 or command[0] != '(') {
917 return error.CodePagePragmaMissingLeftParen;
918 }
919 command = command[1..];
920
921 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
922 command = command[1..];
923 }
924
925 var num_str: []u8 = command[0..0];
926 while (command.len > 0 and (command[0] != ')' and !std.ascii.isWhitespace(command[0]))) {
927 command = command[1..];
928 num_str.len += 1;
929 }
930
931 if (num_str.len == 0) {
932 return error.CodePagePragmaNotInteger;
933 }
934
935 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
936 command = command[1..];
937 }
938
939 if (command.len == 0 or command[0] != ')') {
940 return error.CodePagePragmaMissingRightParen;
941 }
942
943 const code_page = code_page: {
944 if (std.ascii.eqlIgnoreCase("DEFAULT", num_str)) {
945 break :code_page self.default_code_page;
946 }
947
948 // The Win32 compiler behaves fairly strangely around maxInt(u32):
949 // - If the overflowed u32 wraps and becomes a known code page ID, then
950 // it will error/warn with "Codepage not valid: ignored" (depending on /w)
951 // - If the overflowed u32 wraps and does not become a known code page ID,
952 // then it will error with 'constant too big' and 'Codepage not integer'
953 //
954 // Instead of that, we just have a separate error specifically for overflow.
955 const num = parseCodePageNum(num_str) catch |err| switch (err) {
956 error.InvalidCharacter => return error.CodePagePragmaNotInteger,
957 error.Overflow => return error.CodePagePragmaOverflow,
958 };
959
960 // Anything that starts with 0 but does not resolve to 0 is treated as invalid, e.g. 01252
961 if (num_str[0] == '0' and num != 0) {
962 return error.CodePagePragmaInvalidCodePage;
963 }
964 // Anything that resolves to 0 is treated as 'not an integer' by the Win32 implementation.
965 else if (num == 0) {
966 return error.CodePagePragmaNotInteger;
967 }
968 // Anything above u16 max is not going to be found since our CodePage enum is backed by a u16.
969 if (num > std.math.maxInt(u16)) {
970 return error.CodePagePragmaInvalidCodePage;
971 }
972
973 break :code_page code_pages.CodePage.getByIdentifierEnsureSupported(@intCast(num)) catch |err| switch (err) {
974 error.InvalidCodePage => return error.CodePagePragmaInvalidCodePage,
975 error.UnsupportedCodePage => return error.CodePagePragmaUnsupportedCodePage,
976 };
977 };
978
979 // https://learn.microsoft.com/en-us/windows/win32/menurc/pragma-directives
980 // > This pragma is not supported in an included resource file (.rc)
981 //
982 // Even though the Win32 behavior is to just ignore such directives silently,
983 // this is an error in the lexer to allow for emitting warnings/errors when
984 // such directives are found if that's wanted. The intention is for the lexer
985 // to still be able to work correctly after this error is returned.
986 if (self.source_mappings) |source_mappings| {
987 if (!source_mappings.isRootFile(token.line_number)) {
988 return error.CodePagePragmaInIncludedFile;
989 }
990 }
991
992 self.seen_pragma_code_pages +|= 1;
993 self.current_code_page = code_page;
994 }
995
996 fn parseCodePageNum(str: []const u8) !u32 {
997 var x: u32 = 0;
998 for (str) |c| {
999 const digit = try std.fmt.charToDigit(c, 10);
1000 if (x != 0) x = try std.math.mul(u32, x, 10);
1001 x = try std.math.add(u32, x, digit);
1002 }
1003 return x;
1004 }
1005
1006 pub fn getErrorDetails(self: Self, lex_err: LexError) ErrorDetails {
1007 const err = switch (lex_err) {
1008 error.UnfinishedStringLiteral => ErrorDetails.Error.unfinished_string_literal,
1009 error.StringLiteralTooLong => return .{
1010 .err = .string_literal_too_long,
1011 .token = self.error_context_token.?,
1012 .extra = .{ .number = self.max_string_literal_codepoints },
1013 },
1014 error.InvalidNumberWithExponent => ErrorDetails.Error.invalid_number_with_exponent,
1015 error.InvalidDigitCharacterInNumberLiteral => ErrorDetails.Error.invalid_digit_character_in_number_literal,
1016 error.IllegalByte => ErrorDetails.Error.illegal_byte,
1017 error.IllegalByteOutsideStringLiterals => ErrorDetails.Error.illegal_byte_outside_string_literals,
1018 error.IllegalCodepointOutsideStringLiterals => ErrorDetails.Error.illegal_codepoint_outside_string_literals,
1019 error.IllegalByteOrderMark => ErrorDetails.Error.illegal_byte_order_mark,
1020 error.IllegalPrivateUseCharacter => ErrorDetails.Error.illegal_private_use_character,
1021 error.FoundCStyleEscapedQuote => ErrorDetails.Error.found_c_style_escaped_quote,
1022 error.CodePagePragmaMissingLeftParen => ErrorDetails.Error.code_page_pragma_missing_left_paren,
1023 error.CodePagePragmaMissingRightParen => ErrorDetails.Error.code_page_pragma_missing_right_paren,
1024 error.CodePagePragmaInvalidCodePage => ErrorDetails.Error.code_page_pragma_invalid_code_page,
1025 error.CodePagePragmaNotInteger => ErrorDetails.Error.code_page_pragma_not_integer,
1026 error.CodePagePragmaOverflow => ErrorDetails.Error.code_page_pragma_overflow,
1027 error.CodePagePragmaUnsupportedCodePage => ErrorDetails.Error.code_page_pragma_unsupported_code_page,
1028 error.CodePagePragmaInIncludedFile => ErrorDetails.Error.code_page_pragma_in_included_file,
1029 };
1030 return .{
1031 .err = err,
1032 .token = self.error_context_token.?,
1033 };
1034 }
1035};
1036
1037fn testLexNormal(source: []const u8, expected_tokens: []const Token.Id) !void {
1038 var lexer = Lexer.init(source, .{});
1039 if (dumpTokensDuringTests) std.debug.print("\n----------------------\n{s}\n----------------------\n", .{lexer.buffer});
1040 for (expected_tokens) |expected_token_id| {
1041 const token = try lexer.nextNormal();
1042 if (dumpTokensDuringTests) lexer.dump(&token);
1043 try std.testing.expectEqual(expected_token_id, token.id);
1044 }
1045 const last_token = try lexer.nextNormal();
1046 try std.testing.expectEqual(Token.Id.eof, last_token.id);
1047}
1048
1049fn expectLexError(expected: LexError, actual: anytype) !void {
1050 try std.testing.expectError(expected, actual);
1051 if (dumpTokensDuringTests) std.debug.print("{!}\n", .{actual});
1052}
1053
1054test "normal: numbers" {
1055 try testLexNormal("1", &.{.number});
1056 try testLexNormal("-1", &.{.number});
1057 try testLexNormal("- 1", &.{ .number, .number });
1058 try testLexNormal("-a", &.{.number});
1059}
1060
1061test "normal: string literals" {
1062 try testLexNormal("\"\"", &.{.quoted_ascii_string});
1063 // "" is an escaped "
1064 try testLexNormal("\" \"\" \"", &.{.quoted_ascii_string});
1065}
1066
1067test "superscript chars and code pages" {
1068 const firstToken = struct {
1069 pub fn firstToken(source: []const u8, default_code_page: CodePage, comptime lex_method: Lexer.LexMethod) LexError!Token {
1070 var lexer = Lexer.init(source, .{ .default_code_page = default_code_page });
1071 return lexer.next(lex_method);
1072 }
1073 }.firstToken;
1074 const utf8_source = "²";
1075 const windows1252_source = "\xB2";
1076
1077 const windows1252_encoded_as_windows1252 = firstToken(windows1252_source, .windows1252, .normal);
1078 try std.testing.expectError(error.InvalidDigitCharacterInNumberLiteral, windows1252_encoded_as_windows1252);
1079
1080 const utf8_encoded_as_windows1252 = try firstToken(utf8_source, .windows1252, .normal);
1081 try std.testing.expectEqual(Token{
1082 .id = .literal,
1083 .start = 0,
1084 .end = 2,
1085 .line_number = 1,
1086 }, utf8_encoded_as_windows1252);
1087
1088 const utf8_encoded_as_utf8 = firstToken(utf8_source, .utf8, .normal);
1089 try std.testing.expectError(error.InvalidDigitCharacterInNumberLiteral, utf8_encoded_as_utf8);
1090
1091 const windows1252_encoded_as_utf8 = try firstToken(windows1252_source, .utf8, .normal);
1092 try std.testing.expectEqual(Token{
1093 .id = .literal,
1094 .start = 0,
1095 .end = 1,
1096 .line_number = 1,
1097 }, windows1252_encoded_as_utf8);
1098}
src/resinator/literals.zig deleted-911
...@@ -1,911 +0,0 @@
1const std = @import("std");
2const code_pages = @import("code_pages.zig");
3const CodePage = code_pages.CodePage;
4const windows1252 = @import("windows1252.zig");
5const ErrorDetails = @import("errors.zig").ErrorDetails;
6const DiagnosticsContext = @import("errors.zig").DiagnosticsContext;
7const Token = @import("lex.zig").Token;
8
9/// rc is maximally liberal in terms of what it accepts as a number literal
10/// for data values. As long as it starts with a number or - or ~, that's good enough.
11pub fn isValidNumberDataLiteral(str: []const u8) bool {
12 if (str.len == 0) return false;
13 switch (str[0]) {
14 '~', '-', '0'...'9' => return true,
15 else => return false,
16 }
17}
18
19pub const SourceBytes = struct {
20 slice: []const u8,
21 code_page: CodePage,
22};
23
24pub const StringType = enum { ascii, wide };
25
26/// Valid escapes:
27/// "" -> "
28/// \a, \A => 0x08 (not 0x07 like in C)
29/// \n => 0x0A
30/// \r => 0x0D
31/// \t, \T => 0x09
32/// \\ => \
33/// \nnn => byte with numeric value given by nnn interpreted as octal
34/// (wraps on overflow, number of digits can be 1-3 for ASCII strings
35/// and 1-7 for wide strings)
36/// \xhh => byte with numeric value given by hh interpreted as hex
37/// (number of digits can be 0-2 for ASCII strings and 0-4 for
38/// wide strings)
39/// \<\r+> => \
40/// \<[\r\n\t ]+> => <nothing>
41///
42/// Special cases:
43/// <\t> => 1-8 spaces, dependent on columns in the source rc file itself
44/// <\r> => <nothing>
45/// <\n+><\w+?\n?> => <space><\n>
46///
47/// Special, especially weird case:
48/// \"" => "
49/// NOTE: This leads to footguns because the preprocessor can start parsing things
50/// out-of-sync with the RC compiler, expanding macros within string literals, etc.
51/// This parse function handles this case the same as the Windows RC compiler, but
52/// \" within a string literal is treated as an error by the lexer, so the relevant
53/// branches should never actually be hit during this function.
54pub const IterativeStringParser = struct {
55 source: []const u8,
56 code_page: CodePage,
57 /// The type of the string inferred by the prefix (L"" or "")
58 /// This is what matters for things like the maximum digits in an
59 /// escape sequence, whether or not invalid escape sequences are skipped, etc.
60 declared_string_type: StringType,
61 pending_codepoint: ?u21 = null,
62 num_pending_spaces: u8 = 0,
63 index: usize = 0,
64 column: usize = 0,
65 diagnostics: ?DiagnosticsContext = null,
66 seen_tab: bool = false,
67
68 const State = enum {
69 normal,
70 quote,
71 newline,
72 escaped,
73 escaped_cr,
74 escaped_newlines,
75 escaped_octal,
76 escaped_hex,
77 };
78
79 pub fn init(bytes: SourceBytes, options: StringParseOptions) IterativeStringParser {
80 const declared_string_type: StringType = switch (bytes.slice[0]) {
81 'L', 'l' => .wide,
82 else => .ascii,
83 };
84 var source = bytes.slice[1 .. bytes.slice.len - 1]; // remove ""
85 var column = options.start_column + 1; // for the removed "
86 if (declared_string_type == .wide) {
87 source = source[1..]; // remove L
88 column += 1; // for the removed L
89 }
90 return .{
91 .source = source,
92 .code_page = bytes.code_page,
93 .declared_string_type = declared_string_type,
94 .column = column,
95 .diagnostics = options.diagnostics,
96 };
97 }
98
99 pub const ParsedCodepoint = struct {
100 codepoint: u21,
101 from_escaped_integer: bool = false,
102 };
103
104 pub fn next(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
105 const result = try self.nextUnchecked();
106 if (self.diagnostics != null and result != null and !result.?.from_escaped_integer) {
107 switch (result.?.codepoint) {
108 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => {
109 const err: ErrorDetails.Error = if (result.?.codepoint == 0xD00)
110 .rc_would_miscompile_codepoint_skip
111 else
112 .rc_would_miscompile_codepoint_byte_swap;
113 try self.diagnostics.?.diagnostics.append(ErrorDetails{
114 .err = err,
115 .type = .warning,
116 .token = self.diagnostics.?.token,
117 .extra = .{ .number = result.?.codepoint },
118 });
119 try self.diagnostics.?.diagnostics.append(ErrorDetails{
120 .err = err,
121 .type = .note,
122 .token = self.diagnostics.?.token,
123 .print_source_line = false,
124 .extra = .{ .number = result.?.codepoint },
125 });
126 },
127 else => {},
128 }
129 }
130 return result;
131 }
132
133 pub fn nextUnchecked(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
134 if (self.num_pending_spaces > 0) {
135 // Ensure that we don't get into this predicament so we can ensure that
136 // the order of processing any pending stuff doesn't matter
137 std.debug.assert(self.pending_codepoint == null);
138 self.num_pending_spaces -= 1;
139 return .{ .codepoint = ' ' };
140 }
141 if (self.pending_codepoint) |pending_codepoint| {
142 self.pending_codepoint = null;
143 return .{ .codepoint = pending_codepoint };
144 }
145 if (self.index >= self.source.len) return null;
146
147 var state: State = .normal;
148 var string_escape_n: u16 = 0;
149 var string_escape_i: u8 = 0;
150 const max_octal_escape_digits: u8 = switch (self.declared_string_type) {
151 .ascii => 3,
152 .wide => 7,
153 };
154 const max_hex_escape_digits: u8 = switch (self.declared_string_type) {
155 .ascii => 2,
156 .wide => 4,
157 };
158
159 while (self.code_page.codepointAt(self.index, self.source)) |codepoint| : (self.index += codepoint.byte_len) {
160 const c = codepoint.value;
161 var backtrack = false;
162 defer {
163 if (backtrack) {
164 self.index -= codepoint.byte_len;
165 } else {
166 if (c == '\t') {
167 self.column += columnsUntilTabStop(self.column, 8);
168 } else {
169 self.column += codepoint.byte_len;
170 }
171 }
172 }
173 switch (state) {
174 .normal => switch (c) {
175 '\\' => state = .escaped,
176 '"' => state = .quote,
177 '\r' => {},
178 '\n' => state = .newline,
179 '\t' => {
180 // Only warn about a tab getting converted to spaces once per string
181 if (self.diagnostics != null and !self.seen_tab) {
182 try self.diagnostics.?.diagnostics.append(ErrorDetails{
183 .err = .tab_converted_to_spaces,
184 .type = .warning,
185 .token = self.diagnostics.?.token,
186 });
187 try self.diagnostics.?.diagnostics.append(ErrorDetails{
188 .err = .tab_converted_to_spaces,
189 .type = .note,
190 .token = self.diagnostics.?.token,
191 .print_source_line = false,
192 });
193 self.seen_tab = true;
194 }
195 const cols = columnsUntilTabStop(self.column, 8);
196 self.num_pending_spaces = @intCast(cols - 1);
197 self.index += codepoint.byte_len;
198 return .{ .codepoint = ' ' };
199 },
200 else => {
201 self.index += codepoint.byte_len;
202 return .{ .codepoint = c };
203 },
204 },
205 .quote => switch (c) {
206 '"' => {
207 // "" => "
208 self.index += codepoint.byte_len;
209 return .{ .codepoint = '"' };
210 },
211 else => unreachable, // this is a bug in the lexer
212 },
213 .newline => switch (c) {
214 '\r', ' ', '\t', '\n', '\x0b', '\x0c', '\xa0' => {},
215 else => {
216 // backtrack so that we handle the current char properly
217 backtrack = true;
218 // <space><newline>
219 self.index += codepoint.byte_len;
220 self.pending_codepoint = '\n';
221 return .{ .codepoint = ' ' };
222 },
223 },
224 .escaped => switch (c) {
225 '\r' => state = .escaped_cr,
226 '\n' => state = .escaped_newlines,
227 '0'...'7' => {
228 string_escape_n = std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
229 string_escape_i = 1;
230 state = .escaped_octal;
231 },
232 'x', 'X' => {
233 string_escape_n = 0;
234 string_escape_i = 0;
235 state = .escaped_hex;
236 },
237 else => {
238 switch (c) {
239 'a', 'A' => {
240 self.index += codepoint.byte_len;
241 return .{ .codepoint = '\x08' };
242 }, // might be a bug in RC, but matches its behavior
243 'n' => {
244 self.index += codepoint.byte_len;
245 return .{ .codepoint = '\n' };
246 },
247 'r' => {
248 self.index += codepoint.byte_len;
249 return .{ .codepoint = '\r' };
250 },
251 't', 'T' => {
252 self.index += codepoint.byte_len;
253 return .{ .codepoint = '\t' };
254 },
255 '\\' => {
256 self.index += codepoint.byte_len;
257 return .{ .codepoint = '\\' };
258 },
259 '"' => {
260 // \" is a special case that doesn't get the \ included,
261 backtrack = true;
262 },
263 else => switch (self.declared_string_type) {
264 .wide => {}, // invalid escape sequences are skipped in wide strings
265 .ascii => {
266 // backtrack so that we handle the current char properly
267 backtrack = true;
268 self.index += codepoint.byte_len;
269 return .{ .codepoint = '\\' };
270 },
271 },
272 }
273 state = .normal;
274 },
275 },
276 .escaped_cr => switch (c) {
277 '\r' => {},
278 '\n' => state = .escaped_newlines,
279 else => {
280 // backtrack so that we handle the current char properly
281 backtrack = true;
282 self.index += codepoint.byte_len;
283 return .{ .codepoint = '\\' };
284 },
285 },
286 .escaped_newlines => switch (c) {
287 '\r', '\n', '\t', ' ', '\x0b', '\x0c', '\xa0' => {},
288 else => {
289 // backtrack so that we handle the current char properly
290 backtrack = true;
291 state = .normal;
292 },
293 },
294 .escaped_octal => switch (c) {
295 '0'...'7' => {
296 string_escape_n *%= 8;
297 string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
298 string_escape_i += 1;
299 if (string_escape_i == max_octal_escape_digits) {
300 const escaped_value = switch (self.declared_string_type) {
301 .ascii => @as(u8, @truncate(string_escape_n)),
302 .wide => string_escape_n,
303 };
304 self.index += codepoint.byte_len;
305 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
306 }
307 },
308 else => {
309 // backtrack so that we handle the current char properly
310 backtrack = true;
311 // write out whatever byte we have parsed so far
312 const escaped_value = switch (self.declared_string_type) {
313 .ascii => @as(u8, @truncate(string_escape_n)),
314 .wide => string_escape_n,
315 };
316 self.index += codepoint.byte_len;
317 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
318 },
319 },
320 .escaped_hex => switch (c) {
321 '0'...'9', 'a'...'f', 'A'...'F' => {
322 string_escape_n *= 16;
323 string_escape_n += std.fmt.charToDigit(@intCast(c), 16) catch unreachable;
324 string_escape_i += 1;
325 if (string_escape_i == max_hex_escape_digits) {
326 const escaped_value = switch (self.declared_string_type) {
327 .ascii => @as(u8, @truncate(string_escape_n)),
328 .wide => string_escape_n,
329 };
330 self.index += codepoint.byte_len;
331 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
332 }
333 },
334 else => {
335 // backtrack so that we handle the current char properly
336 backtrack = true;
337 // write out whatever byte we have parsed so far
338 // (even with 0 actual digits, \x alone parses to 0)
339 const escaped_value = switch (self.declared_string_type) {
340 .ascii => @as(u8, @truncate(string_escape_n)),
341 .wide => string_escape_n,
342 };
343 self.index += codepoint.byte_len;
344 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
345 },
346 },
347 }
348 }
349
350 switch (state) {
351 .normal, .escaped_newlines => {},
352 .newline => {
353 // <space><newline>
354 self.pending_codepoint = '\n';
355 return .{ .codepoint = ' ' };
356 },
357 .escaped, .escaped_cr => return .{ .codepoint = '\\' },
358 .escaped_octal, .escaped_hex => {
359 const escaped_value = switch (self.declared_string_type) {
360 .ascii => @as(u8, @truncate(string_escape_n)),
361 .wide => string_escape_n,
362 };
363 return .{ .codepoint = escaped_value, .from_escaped_integer = true };
364 },
365 .quote => unreachable, // this is a bug in the lexer
366 }
367
368 return null;
369 }
370};
371
372pub const StringParseOptions = struct {
373 start_column: usize = 0,
374 diagnostics: ?DiagnosticsContext = null,
375 output_code_page: CodePage = .windows1252,
376};
377
378pub fn parseQuotedString(
379 comptime literal_type: StringType,
380 allocator: std.mem.Allocator,
381 bytes: SourceBytes,
382 options: StringParseOptions,
383) !(switch (literal_type) {
384 .ascii => []u8,
385 .wide => [:0]u16,
386}) {
387 const T = if (literal_type == .ascii) u8 else u16;
388 std.debug.assert(bytes.slice.len >= 2); // must at least have 2 double quote chars
389
390 var buf = try std.ArrayList(T).initCapacity(allocator, bytes.slice.len);
391 errdefer buf.deinit();
392
393 var iterative_parser = IterativeStringParser.init(bytes, options);
394
395 while (try iterative_parser.next()) |parsed| {
396 const c = parsed.codepoint;
397 if (parsed.from_escaped_integer) {
398 try buf.append(std.mem.nativeToLittle(T, @intCast(c)));
399 } else {
400 switch (literal_type) {
401 .ascii => switch (options.output_code_page) {
402 .windows1252 => {
403 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
404 try buf.append(best_fit);
405 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
406 try buf.append('?');
407 } else {
408 try buf.appendSlice("??");
409 }
410 },
411 .utf8 => {
412 var codepoint_to_encode = c;
413 if (c == code_pages.Codepoint.invalid) {
414 codepoint_to_encode = '�';
415 }
416 var utf8_buf: [4]u8 = undefined;
417 const utf8_len = std.unicode.utf8Encode(codepoint_to_encode, &utf8_buf) catch unreachable;
418 try buf.appendSlice(utf8_buf[0..utf8_len]);
419 },
420 else => unreachable, // Unsupported code page
421 },
422 .wide => {
423 if (c == code_pages.Codepoint.invalid) {
424 try buf.append(std.mem.nativeToLittle(u16, '�'));
425 } else if (c < 0x10000) {
426 const short: u16 = @intCast(c);
427 try buf.append(std.mem.nativeToLittle(u16, short));
428 } else {
429 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
430 try buf.append(std.mem.nativeToLittle(u16, high));
431 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
432 try buf.append(std.mem.nativeToLittle(u16, low));
433 }
434 },
435 }
436 }
437 }
438
439 if (literal_type == .wide) {
440 return buf.toOwnedSliceSentinel(0);
441 } else {
442 return buf.toOwnedSlice();
443 }
444}
445
446pub fn parseQuotedAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 {
447 std.debug.assert(bytes.slice.len >= 2); // ""
448 return parseQuotedString(.ascii, allocator, bytes, options);
449}
450
451pub fn parseQuotedWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {
452 std.debug.assert(bytes.slice.len >= 3); // L""
453 return parseQuotedString(.wide, allocator, bytes, options);
454}
455
456pub fn parseQuotedStringAsWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {
457 std.debug.assert(bytes.slice.len >= 2); // ""
458 return parseQuotedString(.wide, allocator, bytes, options);
459}
460
461pub fn parseQuotedStringAsAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 {
462 std.debug.assert(bytes.slice.len >= 2); // ""
463 return parseQuotedString(.ascii, allocator, bytes, options);
464}
465
466test "parse quoted ascii string" {
467 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
468 defer arena_allocator.deinit();
469 const arena = arena_allocator.allocator();
470
471 try std.testing.expectEqualSlices(u8, "hello", try parseQuotedAsciiString(arena, .{
472 .slice =
473 \\"hello"
474 ,
475 .code_page = .windows1252,
476 }, .{}));
477 // hex with 0 digits
478 try std.testing.expectEqualSlices(u8, "\x00", try parseQuotedAsciiString(arena, .{
479 .slice =
480 \\"\x"
481 ,
482 .code_page = .windows1252,
483 }, .{}));
484 // hex max of 2 digits
485 try std.testing.expectEqualSlices(u8, "\xFFf", try parseQuotedAsciiString(arena, .{
486 .slice =
487 \\"\XfFf"
488 ,
489 .code_page = .windows1252,
490 }, .{}));
491 // octal with invalid octal digit
492 try std.testing.expectEqualSlices(u8, "\x019", try parseQuotedAsciiString(arena, .{
493 .slice =
494 \\"\19"
495 ,
496 .code_page = .windows1252,
497 }, .{}));
498 // escaped quotes
499 try std.testing.expectEqualSlices(u8, " \" ", try parseQuotedAsciiString(arena, .{
500 .slice =
501 \\" "" "
502 ,
503 .code_page = .windows1252,
504 }, .{}));
505 // backslash right before escaped quotes
506 try std.testing.expectEqualSlices(u8, "\"", try parseQuotedAsciiString(arena, .{
507 .slice =
508 \\"\"""
509 ,
510 .code_page = .windows1252,
511 }, .{}));
512 // octal overflow
513 try std.testing.expectEqualSlices(u8, "\x01", try parseQuotedAsciiString(arena, .{
514 .slice =
515 \\"\401"
516 ,
517 .code_page = .windows1252,
518 }, .{}));
519 // escapes
520 try std.testing.expectEqualSlices(u8, "\x08\n\r\t\\", try parseQuotedAsciiString(arena, .{
521 .slice =
522 \\"\a\n\r\t\\"
523 ,
524 .code_page = .windows1252,
525 }, .{}));
526 // uppercase escapes
527 try std.testing.expectEqualSlices(u8, "\x08\\N\\R\t\\", try parseQuotedAsciiString(arena, .{
528 .slice =
529 \\"\A\N\R\T\\"
530 ,
531 .code_page = .windows1252,
532 }, .{}));
533 // backslash on its own
534 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(arena, .{
535 .slice =
536 \\"\"
537 ,
538 .code_page = .windows1252,
539 }, .{}));
540 // unrecognized escapes
541 try std.testing.expectEqualSlices(u8, "\\b", try parseQuotedAsciiString(arena, .{
542 .slice =
543 \\"\b"
544 ,
545 .code_page = .windows1252,
546 }, .{}));
547 // escaped carriage returns
548 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(
549 arena,
550 .{ .slice = "\"\\\r\r\r\r\r\"", .code_page = .windows1252 },
551 .{},
552 ));
553 // escaped newlines
554 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
555 arena,
556 .{ .slice = "\"\\\n\n\n\n\n\"", .code_page = .windows1252 },
557 .{},
558 ));
559 // escaped CRLF pairs
560 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
561 arena,
562 .{ .slice = "\"\\\r\n\r\n\r\n\r\n\r\n\"", .code_page = .windows1252 },
563 .{},
564 ));
565 // escaped newlines with other whitespace
566 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
567 arena,
568 .{ .slice = "\"\\\n \t\r\n \r\t\n \t\"", .code_page = .windows1252 },
569 .{},
570 ));
571 // literal tab characters get converted to spaces (dependent on source file columns)
572 try std.testing.expectEqualSlices(u8, " ", try parseQuotedAsciiString(
573 arena,
574 .{ .slice = "\"\t\"", .code_page = .windows1252 },
575 .{},
576 ));
577 try std.testing.expectEqualSlices(u8, "abc ", try parseQuotedAsciiString(
578 arena,
579 .{ .slice = "\"abc\t\"", .code_page = .windows1252 },
580 .{},
581 ));
582 try std.testing.expectEqualSlices(u8, "abcdefg ", try parseQuotedAsciiString(
583 arena,
584 .{ .slice = "\"abcdefg\t\"", .code_page = .windows1252 },
585 .{},
586 ));
587 try std.testing.expectEqualSlices(u8, "\\ ", try parseQuotedAsciiString(
588 arena,
589 .{ .slice = "\"\\\t\"", .code_page = .windows1252 },
590 .{},
591 ));
592 // literal CR's get dropped
593 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
594 arena,
595 .{ .slice = "\"\r\r\r\r\r\"", .code_page = .windows1252 },
596 .{},
597 ));
598 // contiguous newlines and whitespace get collapsed to <space><newline>
599 try std.testing.expectEqualSlices(u8, " \n", try parseQuotedAsciiString(
600 arena,
601 .{ .slice = "\"\n\r\r \r\n \t \"", .code_page = .windows1252 },
602 .{},
603 ));
604}
605
606test "parse quoted ascii string with utf8 code page" {
607 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
608 defer arena_allocator.deinit();
609 const arena = arena_allocator.allocator();
610
611 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
612 arena,
613 .{ .slice = "\"\"", .code_page = .utf8 },
614 .{},
615 ));
616 // Codepoints that don't have a Windows-1252 representation get converted to ?
617 try std.testing.expectEqualSlices(u8, "?????????", try parseQuotedAsciiString(
618 arena,
619 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
620 .{},
621 ));
622 // Codepoints that have a best fit mapping get converted accordingly,
623 // these are box drawing codepoints
624 try std.testing.expectEqualSlices(u8, "\x2b\x2d\x2b", try parseQuotedAsciiString(
625 arena,
626 .{ .slice = "\"┌─┐\"", .code_page = .utf8 },
627 .{},
628 ));
629 // Invalid UTF-8 gets converted to ? depending on well-formedness
630 try std.testing.expectEqualSlices(u8, "????", try parseQuotedAsciiString(
631 arena,
632 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
633 .{},
634 ));
635 // Codepoints that would require a UTF-16 surrogate pair get converted to ??
636 try std.testing.expectEqualSlices(u8, "??", try parseQuotedAsciiString(
637 arena,
638 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
639 .{},
640 ));
641
642 // Output code page changes how invalid UTF-8 gets converted, since it
643 // now encodes the result as UTF-8 so it can write replacement characters.
644 try std.testing.expectEqualSlices(u8, "����", try parseQuotedAsciiString(
645 arena,
646 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
647 .{ .output_code_page = .utf8 },
648 ));
649 try std.testing.expectEqualSlices(u8, "\xF2\xAF\xBA\xB4", try parseQuotedAsciiString(
650 arena,
651 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
652 .{ .output_code_page = .utf8 },
653 ));
654}
655
656test "parse quoted wide string" {
657 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
658 defer arena_allocator.deinit();
659 const arena = arena_allocator.allocator();
660
661 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("hello"), 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{ std.mem.nativeToLittle(u16, 0xFFFF), std.mem.nativeToLittle(u16, '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{ std.mem.nativeToLittle(u16, 0x9493), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '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{std.mem.nativeToLittle(u16, 0xFF01)}, try parseQuotedWideString(arena, .{
690 .slice =
691 \\L"\777401"
692 ,
693 .code_page = .windows1252,
694 }, .{}));
695 // literal tab characters get converted to spaces (dependent on source file columns)
696 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("abcdefg "), try parseQuotedWideString(
697 arena,
698 .{ .slice = "L\"abcdefg\t\"", .code_page = .windows1252 },
699 .{},
700 ));
701 // Windows-1252 conversion
702 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("ðð€€€"), try parseQuotedWideString(
703 arena,
704 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .windows1252 },
705 .{},
706 ));
707 // Invalid escape sequences are skipped
708 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedWideString(
709 arena,
710 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
711 .{},
712 ));
713}
714
715test "parse quoted wide string with utf8 code page" {
716 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
717 defer arena_allocator.deinit();
718 const arena = arena_allocator.allocator();
719
720 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{}, try parseQuotedWideString(
721 arena,
722 .{ .slice = "L\"\"", .code_page = .utf8 },
723 .{},
724 ));
725 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedWideString(
726 arena,
727 .{ .slice = "L\"кириллица\"", .code_page = .utf8 },
728 .{},
729 ));
730 // Invalid UTF-8 gets converted to � depending on well-formedness
731 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("����"), try parseQuotedWideString(
732 arena,
733 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
734 .{},
735 ));
736}
737
738test "parse quoted ascii string as wide string" {
739 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
740 defer arena_allocator.deinit();
741 const arena = arena_allocator.allocator();
742
743 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedStringAsWideString(
744 arena,
745 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
746 .{},
747 ));
748 // Whether or not invalid escapes are skipped is still determined by the L prefix
749 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("\\H"), try parseQuotedStringAsWideString(
750 arena,
751 .{ .slice = "\"\\H\"", .code_page = .windows1252 },
752 .{},
753 ));
754 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedStringAsWideString(
755 arena,
756 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
757 .{},
758 ));
759 // Maximum escape sequence value is also determined by the L prefix
760 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0x12), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '4') }, try parseQuotedStringAsWideString(
761 arena,
762 .{ .slice = "\"\\x1234\"", .code_page = .windows1252 },
763 .{},
764 ));
765 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{std.mem.nativeToLittle(u16, 0x1234)}, try parseQuotedStringAsWideString(
766 arena,
767 .{ .slice = "L\"\\x1234\"", .code_page = .windows1252 },
768 .{},
769 ));
770}
771
772pub fn columnsUntilTabStop(column: usize, tab_columns: usize) usize {
773 // 0 => 8, 1 => 7, 2 => 6, 3 => 5, 4 => 4
774 // 5 => 3, 6 => 2, 7 => 1, 8 => 8
775 return tab_columns - (column % tab_columns);
776}
777
778pub fn columnWidth(cur_column: usize, c: u8, tab_columns: usize) usize {
779 return switch (c) {
780 '\t' => columnsUntilTabStop(cur_column, tab_columns),
781 else => 1,
782 };
783}
784
785pub const Number = struct {
786 value: u32,
787 is_long: bool = false,
788
789 pub fn asWord(self: Number) u16 {
790 return @truncate(self.value);
791 }
792
793 pub fn evaluateOperator(lhs: Number, operator_char: u8, rhs: Number) Number {
794 const result = switch (operator_char) {
795 '-' => lhs.value -% rhs.value,
796 '+' => lhs.value +% rhs.value,
797 '|' => lhs.value | rhs.value,
798 '&' => lhs.value & rhs.value,
799 else => unreachable, // invalid operator, this would be a lexer/parser bug
800 };
801 return .{
802 .value = result,
803 .is_long = lhs.is_long or rhs.is_long,
804 };
805 }
806};
807
808/// Assumes that number literals normally rejected by RC's preprocessor
809/// are similarly rejected before being parsed.
810///
811/// Relevant RC preprocessor errors:
812/// RC2021: expected exponent value, not '<digit>'
813/// example that is rejected: 1e1
814/// example that is accepted: 1ea
815/// (this function will parse the two examples above the same)
816pub fn parseNumberLiteral(bytes: SourceBytes) Number {
817 std.debug.assert(bytes.slice.len > 0);
818 var result = Number{ .value = 0, .is_long = false };
819 var radix: u8 = 10;
820 var buf = bytes.slice;
821
822 const Prefix = enum { none, minus, complement };
823 var prefix: Prefix = .none;
824 switch (buf[0]) {
825 '-' => {
826 prefix = .minus;
827 buf = buf[1..];
828 },
829 '~' => {
830 prefix = .complement;
831 buf = buf[1..];
832 },
833 else => {},
834 }
835
836 if (buf.len > 2 and buf[0] == '0') {
837 switch (buf[1]) {
838 'o' => { // octal radix prefix is case-sensitive
839 radix = 8;
840 buf = buf[2..];
841 },
842 'x', 'X' => {
843 radix = 16;
844 buf = buf[2..];
845 },
846 else => {},
847 }
848 }
849
850 var i: usize = 0;
851 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
852 const c = codepoint.value;
853 if (c == 'L' or c == 'l') {
854 result.is_long = true;
855 break;
856 }
857 const digit = switch (c) {
858 // On invalid digit for the radix, just stop parsing but don't fail
859 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch break,
860 else => break,
861 };
862
863 if (result.value != 0) {
864 result.value *%= radix;
865 }
866 result.value +%= digit;
867 }
868
869 switch (prefix) {
870 .none => {},
871 .minus => result.value = 0 -% result.value,
872 .complement => result.value = ~result.value,
873 }
874
875 return result;
876}
877
878test "parse number literal" {
879 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "0", .code_page = .windows1252 }));
880 try std.testing.expectEqual(Number{ .value = 1, .is_long = false }, parseNumberLiteral(.{ .slice = "1", .code_page = .windows1252 }));
881 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "1L", .code_page = .windows1252 }));
882 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "1l", .code_page = .windows1252 }));
883 try std.testing.expectEqual(Number{ .value = 1, .is_long = false }, parseNumberLiteral(.{ .slice = "1garbageL", .code_page = .windows1252 }));
884 try std.testing.expectEqual(Number{ .value = 4294967295, .is_long = false }, parseNumberLiteral(.{ .slice = "4294967295", .code_page = .windows1252 }));
885 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "4294967296", .code_page = .windows1252 }));
886 try std.testing.expectEqual(Number{ .value = 1, .is_long = true }, parseNumberLiteral(.{ .slice = "4294967297L", .code_page = .windows1252 }));
887
888 // can handle any length of number, wraps on overflow appropriately
889 const big_overflow = parseNumberLiteral(.{ .slice = "1000000000000000000000000000000000000000000000000000000000000000000000000000000090000000001", .code_page = .windows1252 });
890 try std.testing.expectEqual(Number{ .value = 4100654081, .is_long = false }, big_overflow);
891 try std.testing.expectEqual(@as(u16, 1025), big_overflow.asWord());
892
893 try std.testing.expectEqual(Number{ .value = 0x20, .is_long = false }, parseNumberLiteral(.{ .slice = "0x20", .code_page = .windows1252 }));
894 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2AL", .code_page = .windows1252 }));
895 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL", .code_page = .windows1252 }));
896 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL", .code_page = .windows1252 }));
897
898 try std.testing.expectEqual(Number{ .value = 0o20, .is_long = false }, parseNumberLiteral(.{ .slice = "0o20", .code_page = .windows1252 }));
899 try std.testing.expectEqual(Number{ .value = 0o20, .is_long = true }, parseNumberLiteral(.{ .slice = "0o20L", .code_page = .windows1252 }));
900 try std.testing.expectEqual(Number{ .value = 0o2, .is_long = false }, parseNumberLiteral(.{ .slice = "0o29", .code_page = .windows1252 }));
901 try std.testing.expectEqual(Number{ .value = 0, .is_long = false }, parseNumberLiteral(.{ .slice = "0O29", .code_page = .windows1252 }));
902
903 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFF, .is_long = false }, parseNumberLiteral(.{ .slice = "-1", .code_page = .windows1252 }));
904 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFE, .is_long = false }, parseNumberLiteral(.{ .slice = "~1", .code_page = .windows1252 }));
905 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFF, .is_long = true }, parseNumberLiteral(.{ .slice = "-4294967297L", .code_page = .windows1252 }));
906 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFE, .is_long = true }, parseNumberLiteral(.{ .slice = "~4294967297L", .code_page = .windows1252 }));
907 try std.testing.expectEqual(Number{ .value = 0xFFFFFFFD, .is_long = false }, parseNumberLiteral(.{ .slice = "-0X3", .code_page = .windows1252 }));
908
909 // anything after L is ignored
910 try std.testing.expectEqual(Number{ .value = 0x2A, .is_long = true }, parseNumberLiteral(.{ .slice = "0x2aL5", .code_page = .windows1252 }));
911}
src/resinator/parse.zig deleted-1883
...@@ -1,1883 +0,0 @@
1const std = @import("std");
2const Lexer = @import("lex.zig").Lexer;
3const Token = @import("lex.zig").Token;
4const Node = @import("ast.zig").Node;
5const Tree = @import("ast.zig").Tree;
6const CodePageLookup = @import("ast.zig").CodePageLookup;
7const Resource = @import("rc.zig").Resource;
8const Allocator = std.mem.Allocator;
9const ErrorDetails = @import("errors.zig").ErrorDetails;
10const Diagnostics = @import("errors.zig").Diagnostics;
11const SourceBytes = @import("literals.zig").SourceBytes;
12const Compiler = @import("compile.zig").Compiler;
13const rc = @import("rc.zig");
14const res = @import("res.zig");
15
16// TODO: Make these configurable?
17pub const max_nested_menu_level: u32 = 512;
18pub const max_nested_version_level: u32 = 512;
19pub const max_nested_expression_level: u32 = 200;
20
21pub const Parser = struct {
22 const Self = @This();
23
24 lexer: *Lexer,
25 /// values that need to be initialized per-parse
26 state: Parser.State = undefined,
27 options: Parser.Options,
28
29 pub const Error = error{ParseError} || Allocator.Error;
30
31 pub const Options = struct {
32 warn_instead_of_error_on_invalid_code_page: bool = false,
33 };
34
35 pub fn init(lexer: *Lexer, options: Options) Parser {
36 return Parser{
37 .lexer = lexer,
38 .options = options,
39 };
40 }
41
42 pub const State = struct {
43 token: Token,
44 lookahead_lexer: Lexer,
45 allocator: Allocator,
46 arena: Allocator,
47 diagnostics: *Diagnostics,
48 input_code_page_lookup: CodePageLookup,
49 output_code_page_lookup: CodePageLookup,
50 };
51
52 pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree {
53 var arena = std.heap.ArenaAllocator.init(allocator);
54 errdefer arena.deinit();
55
56 self.state = Parser.State{
57 .token = undefined,
58 .lookahead_lexer = undefined,
59 .allocator = allocator,
60 .arena = arena.allocator(),
61 .diagnostics = diagnostics,
62 .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
63 .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
64 };
65
66 const parsed_root = try self.parseRoot();
67
68 const tree = try self.state.arena.create(Tree);
69 tree.* = .{
70 .node = parsed_root,
71 .input_code_pages = self.state.input_code_page_lookup,
72 .output_code_pages = self.state.output_code_page_lookup,
73 .source = self.lexer.buffer,
74 .arena = arena.state,
75 .allocator = allocator,
76 };
77 return tree;
78 }
79
80 fn parseRoot(self: *Self) Error!*Node {
81 var statements = std.ArrayList(*Node).init(self.state.allocator);
82 defer statements.deinit();
83
84 try self.parseStatements(&statements);
85 try self.check(.eof);
86
87 const node = try self.state.arena.create(Node.Root);
88 node.* = .{
89 .body = try self.state.arena.dupe(*Node, statements.items),
90 };
91 return &node.base;
92 }
93
94 fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void {
95 while (true) {
96 try self.nextToken(.whitespace_delimiter_only);
97 if (self.state.token.id == .eof) break;
98 // The Win32 compiler will sometimes try to recover from errors
99 // and then restart parsing afterwards. We don't ever do this
100 // because it almost always leads to unhelpful error messages
101 // (usually it will end up with bogus things like 'file
102 // not found: {')
103 const 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 const 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 const 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 const exstyle: ?*Node = try optional_param_parser.parse(.{ .not_expression_allowed = true });
938 const 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_buffer: [4]*Node = undefined;
1250 var parts = std.ArrayListUnmanaged(*Node).initBuffer(&parts_buffer);
1251
1252 while (true) {
1253 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1254 parts.addOneAssumeCapacity().* = value;
1255
1256 if (parts.unusedCapacitySlice().len == 0 or
1257 !(try self.parseOptionalToken(.comma)))
1258 {
1259 break;
1260 }
1261 }
1262
1263 const node = try self.state.arena.create(Node.VersionStatement);
1264 node.* = .{
1265 .type = type_token,
1266 .parts = try self.state.arena.dupe(*Node, parts.items),
1267 };
1268 return &node.base;
1269 },
1270 else => {
1271 const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1272
1273 const node = try self.state.arena.create(Node.SimpleStatement);
1274 node.* = .{
1275 .identifier = type_token,
1276 .value = value,
1277 };
1278 return &node.base;
1279 },
1280 }
1281 }
1282
1283 /// Expects the current token to be handled, and that the version BLOCK/VALUE will
1284 /// begin on the next token.
1285 /// After return, the current token will be the token immediately before the end of the
1286 /// version BLOCK/VALUE (or unchanged if the function returns null).
1287 fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node {
1288 const keyword_token = try self.lookaheadToken(.normal);
1289 const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
1290 self.nextToken(.normal) catch unreachable;
1291
1292 if (nesting_level > max_nested_version_level) {
1293 try self.addErrorDetails(.{
1294 .err = .nested_resource_level_exceeds_max,
1295 .token = top_level_version_id_token,
1296 .extra = .{ .resource = .versioninfo },
1297 });
1298 return self.addErrorDetailsAndFail(.{
1299 .err = .nested_resource_level_exceeds_max,
1300 .type = .note,
1301 .token = keyword_token,
1302 .extra = .{ .resource = .versioninfo },
1303 });
1304 }
1305
1306 try self.nextToken(.normal);
1307 const key = self.state.token;
1308 if (!key.isStringLiteral()) {
1309 return self.addErrorDetailsAndFail(.{
1310 .err = .expected_something_else,
1311 .token = key,
1312 .extra = .{ .expected_types = .{
1313 .string_literal = true,
1314 } },
1315 });
1316 }
1317 // Need to keep track of this to detect a potential miscompilation when
1318 // the comma is omitted and the first value is a quoted string.
1319 const had_comma_before_first_value = try self.parseOptionalToken(.comma);
1320 try self.skipAnyCommas();
1321
1322 const values = try self.parseBlockValuesList(had_comma_before_first_value);
1323
1324 switch (keyword) {
1325 .block => {
1326 try self.nextToken(.normal);
1327 const begin_token = self.state.token;
1328 try self.check(.begin);
1329
1330 var children = std.ArrayListUnmanaged(*Node){};
1331 while (try self.parseVersionBlockOrValue(top_level_version_id_token, nesting_level + 1)) |value_node| {
1332 try children.append(self.state.arena, value_node);
1333 }
1334
1335 try self.nextToken(.normal);
1336 const end_token = self.state.token;
1337 try self.check(.end);
1338
1339 const node = try self.state.arena.create(Node.Block);
1340 node.* = .{
1341 .identifier = keyword_token,
1342 .key = key,
1343 .values = values,
1344 .begin_token = begin_token,
1345 .children = try children.toOwnedSlice(self.state.arena),
1346 .end_token = end_token,
1347 };
1348 return &node.base;
1349 },
1350 .value => {
1351 const node = try self.state.arena.create(Node.BlockValue);
1352 node.* = .{
1353 .identifier = keyword_token,
1354 .key = key,
1355 .values = values,
1356 };
1357 return &node.base;
1358 },
1359 }
1360 }
1361
1362 fn parseBlockValuesList(self: *Self, had_comma_before_first_value: bool) Error![]*Node {
1363 var values = std.ArrayListUnmanaged(*Node){};
1364 var seen_number: bool = false;
1365 var first_string_value: ?*Node = null;
1366 while (true) {
1367 const lookahead_token = try self.lookaheadToken(.normal);
1368 switch (lookahead_token.id) {
1369 .operator,
1370 .number,
1371 .open_paren,
1372 .quoted_ascii_string,
1373 .quoted_wide_string,
1374 => {},
1375 else => break,
1376 }
1377 const value = try self.parseExpression(.{});
1378
1379 if (value.isNumberExpression()) {
1380 seen_number = true;
1381 } else if (first_string_value == null) {
1382 std.debug.assert(value.isStringLiteral());
1383 first_string_value = value;
1384 }
1385
1386 const has_trailing_comma = try self.parseOptionalToken(.comma);
1387 try self.skipAnyCommas();
1388
1389 const value_value = try self.state.arena.create(Node.BlockValueValue);
1390 value_value.* = .{
1391 .expression = value,
1392 .trailing_comma = has_trailing_comma,
1393 };
1394 try values.append(self.state.arena, &value_value.base);
1395 }
1396 if (seen_number and first_string_value != null) {
1397 // The Win32 RC compiler does some strange stuff with the data size:
1398 // Strings are counted as UTF-16 code units including the null-terminator
1399 // Numbers are counted as their byte lengths
1400 // So, when both strings and numbers are within a single value,
1401 // it incorrectly sets the value's type as binary, but then gives the
1402 // data length as a mixture of bytes and UTF-16 code units. This means that
1403 // when the length is read, it will be treated as byte length and will
1404 // not read the full value. We don't reproduce this behavior, so we warn
1405 // of the miscompilation here.
1406 try self.addErrorDetails(.{
1407 .err = .rc_would_miscompile_version_value_byte_count,
1408 .type = .warning,
1409 .token = first_string_value.?.getFirstToken(),
1410 .token_span_start = values.items[0].getFirstToken(),
1411 .token_span_end = values.items[values.items.len - 1].getLastToken(),
1412 });
1413 try self.addErrorDetails(.{
1414 .err = .rc_would_miscompile_version_value_byte_count,
1415 .type = .note,
1416 .token = first_string_value.?.getFirstToken(),
1417 .token_span_start = values.items[0].getFirstToken(),
1418 .token_span_end = values.items[values.items.len - 1].getLastToken(),
1419 .print_source_line = false,
1420 });
1421 }
1422 if (!had_comma_before_first_value and values.items.len > 0 and values.items[0].cast(.block_value_value).?.expression.isStringLiteral()) {
1423 const token = values.items[0].cast(.block_value_value).?.expression.cast(.literal).?.token;
1424 try self.addErrorDetails(.{
1425 .err = .rc_would_miscompile_version_value_padding,
1426 .type = .warning,
1427 .token = token,
1428 });
1429 try self.addErrorDetails(.{
1430 .err = .rc_would_miscompile_version_value_padding,
1431 .type = .note,
1432 .token = token,
1433 .print_source_line = false,
1434 });
1435 }
1436 return values.toOwnedSlice(self.state.arena);
1437 }
1438
1439 fn numberExpressionContainsAnyLSuffixes(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) bool {
1440 // TODO: This could probably be done without evaluating the whole expression
1441 return Compiler.evaluateNumberExpression(expression_node, source, code_page_lookup).is_long;
1442 }
1443
1444 /// Expects the current token to be a literal token that contains the string LANGUAGE
1445 fn parseLanguageStatement(self: *Self) Error!*Node {
1446 const language_token = self.state.token;
1447
1448 const primary_language = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1449
1450 try self.nextToken(.normal);
1451 try self.check(.comma);
1452
1453 const sublanguage = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
1454
1455 // The Win32 RC compiler errors if either parameter contains any number with an L
1456 // suffix. Instead of that, we want to warn and then let the values get truncated.
1457 // The warning is done here to allow the compiler logic to not have to deal with this.
1458 if (numberExpressionContainsAnyLSuffixes(primary_language, self.lexer.buffer, &self.state.input_code_page_lookup)) {
1459 try self.addErrorDetails(.{
1460 .err = .rc_would_error_u16_with_l_suffix,
1461 .type = .warning,
1462 .token = primary_language.getFirstToken(),
1463 .token_span_end = primary_language.getLastToken(),
1464 .extra = .{ .statement_with_u16_param = .language },
1465 });
1466 try self.addErrorDetails(.{
1467 .err = .rc_would_error_u16_with_l_suffix,
1468 .print_source_line = false,
1469 .type = .note,
1470 .token = primary_language.getFirstToken(),
1471 .token_span_end = primary_language.getLastToken(),
1472 .extra = .{ .statement_with_u16_param = .language },
1473 });
1474 }
1475 if (numberExpressionContainsAnyLSuffixes(sublanguage, self.lexer.buffer, &self.state.input_code_page_lookup)) {
1476 try self.addErrorDetails(.{
1477 .err = .rc_would_error_u16_with_l_suffix,
1478 .type = .warning,
1479 .token = sublanguage.getFirstToken(),
1480 .token_span_end = sublanguage.getLastToken(),
1481 .extra = .{ .statement_with_u16_param = .language },
1482 });
1483 try self.addErrorDetails(.{
1484 .err = .rc_would_error_u16_with_l_suffix,
1485 .print_source_line = false,
1486 .type = .note,
1487 .token = sublanguage.getFirstToken(),
1488 .token_span_end = sublanguage.getLastToken(),
1489 .extra = .{ .statement_with_u16_param = .language },
1490 });
1491 }
1492
1493 const node = try self.state.arena.create(Node.LanguageStatement);
1494 node.* = .{
1495 .language_token = language_token,
1496 .primary_language_id = primary_language,
1497 .sublanguage_id = sublanguage,
1498 };
1499 return &node.base;
1500 }
1501
1502 pub const ParseExpressionOptions = struct {
1503 is_known_to_be_number_expression: bool = false,
1504 can_contain_not_expressions: bool = false,
1505 nesting_context: NestingContext = .{},
1506 allowed_types: AllowedTypes = .{ .literal = true, .number = true, .string = true },
1507 expected_types_override: ?ErrorDetails.ExpectedTypes = null,
1508
1509 pub const AllowedTypes = struct {
1510 literal: bool = false,
1511 number: bool = false,
1512 string: bool = false,
1513 };
1514
1515 pub const NestingContext = struct {
1516 first_token: ?Token = null,
1517 last_token: ?Token = null,
1518 level: u32 = 0,
1519
1520 /// Returns a new NestingContext with values modified appropriately for an increased nesting level
1521 fn incremented(ctx: NestingContext, first_token: Token, most_recent_token: Token) NestingContext {
1522 return .{
1523 .first_token = ctx.first_token orelse first_token,
1524 .last_token = most_recent_token,
1525 .level = ctx.level + 1,
1526 };
1527 }
1528 };
1529
1530 pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetails {
1531 // TODO: expected_types_override interaction with is_known_to_be_number_expression?
1532 const expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{
1533 .number = options.allowed_types.number,
1534 .number_expression = options.allowed_types.number,
1535 .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression,
1536 .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression,
1537 };
1538 return ErrorDetails{
1539 .err = .expected_something_else,
1540 .token = token,
1541 .extra = .{ .expected_types = expected_types },
1542 };
1543 }
1544 };
1545
1546 /// Returns true if the next lookahead token is a number or could be the start of a number expression.
1547 /// Only useful when looking for empty expressions in optional fields.
1548 fn lookaheadCouldBeNumberExpression(self: *Self, not_allowed: enum { not_allowed, not_disallowed }) Error!bool {
1549 var lookahead_token = try self.lookaheadToken(.normal);
1550 switch (lookahead_token.id) {
1551 .literal => if (not_allowed == .not_allowed) {
1552 return std.ascii.eqlIgnoreCase("NOT", lookahead_token.slice(self.lexer.buffer));
1553 } else return false,
1554 .number => return true,
1555 .open_paren => return true,
1556 .operator => {
1557 // + can be a unary operator, see parseExpression's handling of unary +
1558 const operator_char = lookahead_token.slice(self.lexer.buffer)[0];
1559 return operator_char == '+';
1560 },
1561 else => return false,
1562 }
1563 }
1564
1565 fn parsePrimary(self: *Self, options: ParseExpressionOptions) Error!*Node {
1566 try self.nextToken(.normal);
1567 const first_token = self.state.token;
1568 var is_close_paren_expression = false;
1569 var is_unary_plus_expression = false;
1570 switch (self.state.token.id) {
1571 .quoted_ascii_string, .quoted_wide_string => {
1572 if (!options.allowed_types.string) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1573 const node = try self.state.arena.create(Node.Literal);
1574 node.* = .{ .token = self.state.token };
1575 return &node.base;
1576 },
1577 .literal => {
1578 if (options.can_contain_not_expressions and std.ascii.eqlIgnoreCase("NOT", self.state.token.slice(self.lexer.buffer))) {
1579 const not_token = self.state.token;
1580 try self.nextToken(.normal);
1581 try self.check(.number);
1582 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1583 const node = try self.state.arena.create(Node.NotExpression);
1584 node.* = .{
1585 .not_token = not_token,
1586 .number_token = self.state.token,
1587 };
1588 return &node.base;
1589 }
1590 if (!options.allowed_types.literal) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1591 const node = try self.state.arena.create(Node.Literal);
1592 node.* = .{ .token = self.state.token };
1593 return &node.base;
1594 },
1595 .number => {
1596 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
1597 const node = try self.state.arena.create(Node.Literal);
1598 node.* = .{ .token = self.state.token };
1599 return &node.base;
1600 },
1601 .open_paren => {
1602 const open_paren_token = self.state.token;
1603
1604 const expression = try self.parseExpression(.{
1605 .is_known_to_be_number_expression = true,
1606 .can_contain_not_expressions = options.can_contain_not_expressions,
1607 .nesting_context = options.nesting_context.incremented(first_token, open_paren_token),
1608 .allowed_types = .{ .number = true },
1609 });
1610
1611 try self.nextToken(.normal);
1612 // TODO: Add context to error about where the open paren is
1613 try self.check(.close_paren);
1614
1615 if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(open_paren_token));
1616 const node = try self.state.arena.create(Node.GroupedExpression);
1617 node.* = .{
1618 .open_token = open_paren_token,
1619 .expression = expression,
1620 .close_token = self.state.token,
1621 };
1622 return &node.base;
1623 },
1624 .close_paren => {
1625 // Note: In the Win32 implementation, a single close paren
1626 // counts as a valid "expression", but only when its the first and
1627 // only token in the expression. Such an expression is then treated
1628 // as a 'skip this expression' instruction. For example:
1629 // 1 RCDATA { 1, ), ), ), 2 }
1630 // will be evaluated as if it were `1 RCDATA { 1, 2 }` and only
1631 // 0x0001 and 0x0002 will be written to the .res data.
1632 //
1633 // This behavior is not emulated because it almost certainly has
1634 // no valid use cases and only introduces edge cases that are
1635 // not worth the effort to track down and deal with. Instead,
1636 // we error but also add a note about the Win32 RC behavior if
1637 // this edge case is detected.
1638 if (!options.is_known_to_be_number_expression) {
1639 is_close_paren_expression = true;
1640 }
1641 },
1642 .operator => {
1643 // In the Win32 implementation, something akin to a unary +
1644 // is allowed but it doesn't behave exactly like a unary +.
1645 // Instead of emulating the Win32 behavior, we instead error
1646 // and add a note about unary plus not being allowed.
1647 //
1648 // This is done because unary + only works in some places,
1649 // and there's no real use-case for it since it's so limited
1650 // in how it can be used (e.g. +1 is accepted but (+1) will error)
1651 //
1652 // Even understanding when unary plus is allowed is difficult, so
1653 // we don't do any fancy detection of when the Win32 RC compiler would
1654 // allow a unary + and instead just output the note in all cases.
1655 //
1656 // Some examples of allowed expressions by the Win32 compiler:
1657 // +1
1658 // 0|+5
1659 // +1+2
1660 // +~-5
1661 // +(1)
1662 //
1663 // Some examples of disallowed expressions by the Win32 compiler:
1664 // (+1)
1665 // ++5
1666 //
1667 // TODO: Potentially re-evaluate and support the unary plus in a bug-for-bug
1668 // compatible way.
1669 const operator_char = self.state.token.slice(self.lexer.buffer)[0];
1670 if (operator_char == '+') {
1671 is_unary_plus_expression = true;
1672 }
1673 },
1674 else => {},
1675 }
1676
1677 try self.addErrorDetails(options.toErrorDetails(self.state.token));
1678 if (is_close_paren_expression) {
1679 try self.addErrorDetails(ErrorDetails{
1680 .err = .close_paren_expression,
1681 .type = .note,
1682 .token = self.state.token,
1683 .print_source_line = false,
1684 });
1685 }
1686 if (is_unary_plus_expression) {
1687 try self.addErrorDetails(ErrorDetails{
1688 .err = .unary_plus_expression,
1689 .type = .note,
1690 .token = self.state.token,
1691 .print_source_line = false,
1692 });
1693 }
1694 return error.ParseError;
1695 }
1696
1697 /// Expects the current token to have already been dealt with, and that the
1698 /// expression will start on the next token.
1699 /// After return, the current token will have been dealt with.
1700 fn parseExpression(self: *Self, options: ParseExpressionOptions) Error!*Node {
1701 if (options.nesting_context.level > max_nested_expression_level) {
1702 try self.addErrorDetails(.{
1703 .err = .nested_expression_level_exceeds_max,
1704 .token = options.nesting_context.first_token.?,
1705 });
1706 return self.addErrorDetailsAndFail(.{
1707 .err = .nested_expression_level_exceeds_max,
1708 .type = .note,
1709 .token = options.nesting_context.last_token.?,
1710 });
1711 }
1712 var expr: *Node = try self.parsePrimary(options);
1713 const first_token = expr.getFirstToken();
1714
1715 // Non-number expressions can't have operators, so we can just return
1716 if (!expr.isNumberExpression()) return expr;
1717
1718 while (try self.parseOptionalTokenAdvanced(.operator, .normal_expect_operator)) {
1719 const operator = self.state.token;
1720 const rhs_node = try self.parsePrimary(.{
1721 .is_known_to_be_number_expression = true,
1722 .can_contain_not_expressions = options.can_contain_not_expressions,
1723 .nesting_context = options.nesting_context.incremented(first_token, operator),
1724 .allowed_types = options.allowed_types,
1725 });
1726
1727 if (!rhs_node.isNumberExpression()) {
1728 return self.addErrorDetailsAndFail(ErrorDetails{
1729 .err = .expected_something_else,
1730 .token = rhs_node.getFirstToken(),
1731 .token_span_end = rhs_node.getLastToken(),
1732 .extra = .{ .expected_types = .{
1733 .number = true,
1734 .number_expression = true,
1735 } },
1736 });
1737 }
1738
1739 const node = try self.state.arena.create(Node.BinaryExpression);
1740 node.* = .{
1741 .left = expr,
1742 .operator = operator,
1743 .right = rhs_node,
1744 };
1745 expr = &node.base;
1746 }
1747
1748 return expr;
1749 }
1750
1751 /// Skips any amount of commas (including zero)
1752 /// In other words, it will skip the regex `,*`
1753 /// Assumes the token(s) should be parsed with `.normal` as the method.
1754 fn skipAnyCommas(self: *Self) !void {
1755 while (try self.parseOptionalToken(.comma)) {}
1756 }
1757
1758 /// Advances the current token only if the token's id matches the specified `id`.
1759 /// Assumes the token should be parsed with `.normal` as the method.
1760 /// Returns true if the token matched, false otherwise.
1761 fn parseOptionalToken(self: *Self, id: Token.Id) Error!bool {
1762 return self.parseOptionalTokenAdvanced(id, .normal);
1763 }
1764
1765 /// Advances the current token only if the token's id matches the specified `id`.
1766 /// Returns true if the token matched, false otherwise.
1767 fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool {
1768 const maybe_token = try self.lookaheadToken(method);
1769 if (maybe_token.id != id) return false;
1770 self.nextToken(method) catch unreachable;
1771 return true;
1772 }
1773
1774 fn addErrorDetails(self: *Self, details: ErrorDetails) Allocator.Error!void {
1775 try self.state.diagnostics.append(details);
1776 }
1777
1778 fn addErrorDetailsAndFail(self: *Self, details: ErrorDetails) Error {
1779 try self.addErrorDetails(details);
1780 return error.ParseError;
1781 }
1782
1783 fn nextToken(self: *Self, comptime method: Lexer.LexMethod) Error!void {
1784 self.state.token = token: while (true) {
1785 const token = self.lexer.next(method) catch |err| switch (err) {
1786 error.CodePagePragmaInIncludedFile => {
1787 // The Win32 RC compiler silently ignores such `#pragma code_point` directives,
1788 // but we want to both ignore them *and* emit a warning
1789 try self.addErrorDetails(.{
1790 .err = .code_page_pragma_in_included_file,
1791 .type = .warning,
1792 .token = self.lexer.error_context_token.?,
1793 });
1794 continue;
1795 },
1796 error.CodePagePragmaInvalidCodePage => {
1797 var details = self.lexer.getErrorDetails(err);
1798 if (!self.options.warn_instead_of_error_on_invalid_code_page) {
1799 return self.addErrorDetailsAndFail(details);
1800 }
1801 details.type = .warning;
1802 try self.addErrorDetails(details);
1803 continue;
1804 },
1805 error.InvalidDigitCharacterInNumberLiteral => {
1806 const details = self.lexer.getErrorDetails(err);
1807 try self.addErrorDetails(details);
1808 return self.addErrorDetailsAndFail(.{
1809 .err = details.err,
1810 .type = .note,
1811 .token = details.token,
1812 .print_source_line = false,
1813 });
1814 },
1815 else => return self.addErrorDetailsAndFail(self.lexer.getErrorDetails(err)),
1816 };
1817 break :token token;
1818 };
1819 // After every token, set the input code page for its line
1820 try self.state.input_code_page_lookup.setForToken(self.state.token, self.lexer.current_code_page);
1821 // But only set the output code page to the current code page if we are past the first code_page pragma in the file.
1822 // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that
1823 // don't have an explicit output code page set.
1824 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;
1825 try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page);
1826 }
1827
1828 fn lookaheadToken(self: *Self, comptime method: Lexer.LexMethod) Error!Token {
1829 self.state.lookahead_lexer = self.lexer.*;
1830 return token: while (true) {
1831 break :token self.state.lookahead_lexer.next(method) catch |err| switch (err) {
1832 // Ignore this error and get the next valid token, we'll deal with this
1833 // properly when getting the token for real
1834 error.CodePagePragmaInIncludedFile => continue,
1835 else => return self.addErrorDetailsAndFail(self.state.lookahead_lexer.getErrorDetails(err)),
1836 };
1837 };
1838 }
1839
1840 fn tokenSlice(self: *Self) []const u8 {
1841 return self.state.token.slice(self.lexer.buffer);
1842 }
1843
1844 /// Check that the current token is something that can be used as an ID
1845 fn checkId(self: *Self) !void {
1846 switch (self.state.token.id) {
1847 .literal => {},
1848 else => {
1849 return self.addErrorDetailsAndFail(ErrorDetails{
1850 .err = .expected_token,
1851 .token = self.state.token,
1852 .extra = .{ .expected = .literal },
1853 });
1854 },
1855 }
1856 }
1857
1858 fn check(self: *Self, expected_token_id: Token.Id) !void {
1859 if (self.state.token.id != expected_token_id) {
1860 return self.addErrorDetailsAndFail(ErrorDetails{
1861 .err = .expected_token,
1862 .token = self.state.token,
1863 .extra = .{ .expected = expected_token_id },
1864 });
1865 }
1866 }
1867
1868 fn checkResource(self: *Self) !Resource {
1869 switch (self.state.token.id) {
1870 .literal => return Resource.fromString(.{
1871 .slice = self.state.token.slice(self.lexer.buffer),
1872 .code_page = self.lexer.current_code_page,
1873 }),
1874 else => {
1875 return self.addErrorDetailsAndFail(ErrorDetails{
1876 .err = .expected_token,
1877 .token = self.state.token,
1878 .extra = .{ .expected = .literal },
1879 });
1880 },
1881 }
1882 }
1883};
src/resinator/preprocess.zig deleted-100
...@@ -1,100 +0,0 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const cli = @import("cli.zig");
5
6pub const IncludeArgs = struct {
7 clang_target: ?[]const u8 = null,
8 system_include_paths: []const []const u8,
9 /// Should be set to `true` when -target has the GNU abi
10 /// (either because `clang_target` has `-gnu` or `-target`
11 /// is appended via other means and it has `-gnu`)
12 needs_gnu_workaround: bool = false,
13 nostdinc: bool = false,
14
15 pub const IncludeAbi = enum {
16 msvc,
17 gnu,
18 };
19};
20
21/// `arena` is used for temporary -D argument strings and the INCLUDE environment variable.
22/// The arena should be kept alive at least as long as `argv`.
23pub fn appendClangArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options: cli.Options, include_args: IncludeArgs) !void {
24 try argv.appendSlice(&[_][]const u8{
25 "-E", // preprocessor only
26 "--comments",
27 "-fuse-line-directives", // #line <num> instead of # <num>
28 // TODO: could use --trace-includes to give info about what's included from where
29 "-xc", // output c
30 // TODO: Turn this off, check the warnings, and convert the spaces back to NUL
31 "-Werror=null-character", // error on null characters instead of converting them to spaces
32 // TODO: could remove -Werror=null-character and instead parse warnings looking for 'warning: null character ignored'
33 // since the only real problem is when clang doesn't preserve null characters
34 //"-Werror=invalid-pp-token", // will error on unfinished string literals
35 // TODO: could use -Werror instead
36 "-fms-compatibility", // Allow things like "header.h" to be resolved relative to the 'root' .rc file, among other things
37 // https://learn.microsoft.com/en-us/windows/win32/menurc/predefined-macros
38 "-DRC_INVOKED",
39 });
40 for (options.extra_include_paths.items) |extra_include_path| {
41 try argv.append("-I");
42 try argv.append(extra_include_path);
43 }
44
45 if (include_args.nostdinc) {
46 try argv.append("-nostdinc");
47 }
48 for (include_args.system_include_paths) |include_path| {
49 try argv.append("-isystem");
50 try argv.append(include_path);
51 }
52 if (include_args.clang_target) |target| {
53 try argv.append("-target");
54 try argv.append(target);
55 }
56 // Using -fms-compatibility and targeting the GNU abi interact in a strange way:
57 // - Targeting the GNU abi stops _MSC_VER from being defined
58 // - Passing -fms-compatibility stops __GNUC__ from being defined
59 // Neither being defined is a problem for things like MinGW's vadefs.h,
60 // which will fail during preprocessing if neither are defined.
61 // So, when targeting the GNU abi, we need to force __GNUC__ to be defined.
62 //
63 // TODO: This is a workaround that should be removed if possible.
64 if (include_args.needs_gnu_workaround) {
65 // This is the same default gnuc version that Clang uses:
66 // https://github.com/llvm/llvm-project/blob/4b5366c9512aa273a5272af1d833961e1ed156e7/clang/lib/Driver/ToolChains/Clang.cpp#L6738
67 try argv.append("-fgnuc-version=4.2.1");
68 }
69
70 if (!options.ignore_include_env_var) {
71 const INCLUDE = std.process.getEnvVarOwned(arena, "INCLUDE") catch "";
72
73 // The only precedence here is llvm-rc which also uses the platform-specific
74 // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
75 const delimiter = switch (builtin.os.tag) {
76 .windows => ';',
77 else => ':',
78 };
79 var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
80 while (it.next()) |include_path| {
81 try argv.append("-isystem");
82 try argv.append(include_path);
83 }
84 }
85
86 var symbol_it = options.symbols.iterator();
87 while (symbol_it.next()) |entry| {
88 switch (entry.value_ptr.*) {
89 .define => |value| {
90 try argv.append("-D");
91 const define_arg = try std.fmt.allocPrint(arena, "{s}={s}", .{ entry.key_ptr.*, value });
92 try argv.append(define_arg);
93 },
94 .undefine => {
95 try argv.append("-U");
96 try argv.append(entry.key_ptr.*);
97 },
98 }
99 }
100}
src/resinator/rc.zig deleted-407
...@@ -1,407 +0,0 @@
1const std = @import("std");
2const utils = @import("utils.zig");
3const res = @import("res.zig");
4const SourceBytes = @import("literals.zig").SourceBytes;
5
6// https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files
7
8pub const Resource = enum {
9 accelerators,
10 bitmap,
11 cursor,
12 dialog,
13 dialogex,
14 /// As far as I can tell, this is undocumented; the most I could find was this:
15 /// https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/91697
16 dlginclude,
17 /// Undocumented, basically works exactly like RCDATA
18 dlginit,
19 font,
20 html,
21 icon,
22 menu,
23 menuex,
24 messagetable,
25 plugplay, // Obsolete
26 rcdata,
27 stringtable,
28 /// Undocumented
29 toolbar,
30 user_defined,
31 versioninfo,
32 vxd, // Obsolete
33
34 // Types that are treated as a user-defined type when encountered, but have
35 // special meaning without the Visual Studio GUI. We match the Win32 RC compiler
36 // behavior by acting as if these keyword don't exist when compiling the .rc
37 // (thereby treating them as user-defined).
38 //textinclude, // A special resource that is interpreted by Visual C++.
39 //typelib, // A special resource that is used with the /TLBID and /TLBOUT linker options
40
41 // Types that can only be specified by numbers, they don't have keywords
42 cursor_num,
43 icon_num,
44 string_num,
45 anicursor_num,
46 aniicon_num,
47 fontdir_num,
48 manifest_num,
49
50 const map = std.ComptimeStringMapWithEql(Resource, .{
51 .{ "ACCELERATORS", .accelerators },
52 .{ "BITMAP", .bitmap },
53 .{ "CURSOR", .cursor },
54 .{ "DIALOG", .dialog },
55 .{ "DIALOGEX", .dialogex },
56 .{ "DLGINCLUDE", .dlginclude },
57 .{ "DLGINIT", .dlginit },
58 .{ "FONT", .font },
59 .{ "HTML", .html },
60 .{ "ICON", .icon },
61 .{ "MENU", .menu },
62 .{ "MENUEX", .menuex },
63 .{ "MESSAGETABLE", .messagetable },
64 .{ "PLUGPLAY", .plugplay },
65 .{ "RCDATA", .rcdata },
66 .{ "STRINGTABLE", .stringtable },
67 .{ "TOOLBAR", .toolbar },
68 .{ "VERSIONINFO", .versioninfo },
69 .{ "VXD", .vxd },
70 }, std.comptime_string_map.eqlAsciiIgnoreCase);
71
72 pub fn fromString(bytes: SourceBytes) Resource {
73 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes);
74 if (maybe_ordinal) |ordinal| {
75 if (ordinal.ordinal >= 256) return .user_defined;
76 return fromRT(@enumFromInt(ordinal.ordinal));
77 }
78 return map.get(bytes.slice) orelse .user_defined;
79 }
80
81 // TODO: Some comptime validation that RT <-> Resource conversion is synced?
82 pub fn fromRT(rt: res.RT) Resource {
83 return switch (rt) {
84 .ACCELERATOR => .accelerators,
85 .ANICURSOR => .anicursor_num,
86 .ANIICON => .aniicon_num,
87 .BITMAP => .bitmap,
88 .CURSOR => .cursor_num,
89 .DIALOG => .dialog,
90 .DLGINCLUDE => .dlginclude,
91 .DLGINIT => .dlginit,
92 .FONT => .font,
93 .FONTDIR => .fontdir_num,
94 .GROUP_CURSOR => .cursor,
95 .GROUP_ICON => .icon,
96 .HTML => .html,
97 .ICON => .icon_num,
98 .MANIFEST => .manifest_num,
99 .MENU => .menu,
100 .MESSAGETABLE => .messagetable,
101 .PLUGPLAY => .plugplay,
102 .RCDATA => .rcdata,
103 .STRING => .string_num,
104 .TOOLBAR => .toolbar,
105 .VERSION => .versioninfo,
106 .VXD => .vxd,
107 _ => .user_defined,
108 };
109 }
110
111 pub fn canUseRawData(resource: Resource) bool {
112 return switch (resource) {
113 .user_defined,
114 .html,
115 .plugplay, // Obsolete
116 .rcdata,
117 .vxd, // Obsolete
118 .manifest_num,
119 .dlginit,
120 => true,
121 else => false,
122 };
123 }
124
125 pub fn nameForErrorDisplay(resource: Resource) []const u8 {
126 return switch (resource) {
127 // zig fmt: off
128 .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font,
129 .html, .icon, .menu, .menuex, .messagetable, .plugplay, .rcdata, .stringtable,
130 .toolbar, .versioninfo, .vxd => @tagName(resource),
131 // zig fmt: on
132 .user_defined => "user-defined",
133 .cursor_num => std.fmt.comptimePrint("{d} (cursor)", .{@intFromEnum(res.RT.CURSOR)}),
134 .icon_num => std.fmt.comptimePrint("{d} (icon)", .{@intFromEnum(res.RT.ICON)}),
135 .string_num => std.fmt.comptimePrint("{d} (string)", .{@intFromEnum(res.RT.STRING)}),
136 .anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@intFromEnum(res.RT.ANICURSOR)}),
137 .aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@intFromEnum(res.RT.ANIICON)}),
138 .fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@intFromEnum(res.RT.FONTDIR)}),
139 .manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@intFromEnum(res.RT.MANIFEST)}),
140 };
141 }
142};
143
144/// https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable-resource#parameters
145/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialog-resource#parameters
146/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialogex-resource#parameters
147pub const OptionalStatements = enum {
148 characteristics,
149 language,
150 version,
151
152 // DIALOG
153 caption,
154 class,
155 exstyle,
156 font,
157 menu,
158 style,
159
160 pub const map = std.ComptimeStringMapWithEql(OptionalStatements, .{
161 .{ "CHARACTERISTICS", .characteristics },
162 .{ "LANGUAGE", .language },
163 .{ "VERSION", .version },
164 }, std.comptime_string_map.eqlAsciiIgnoreCase);
165
166 pub const dialog_map = std.ComptimeStringMapWithEql(OptionalStatements, .{
167 .{ "CAPTION", .caption },
168 .{ "CLASS", .class },
169 .{ "EXSTYLE", .exstyle },
170 .{ "FONT", .font },
171 .{ "MENU", .menu },
172 .{ "STYLE", .style },
173 }, std.comptime_string_map.eqlAsciiIgnoreCase);
174};
175
176pub const Control = enum {
177 auto3state,
178 autocheckbox,
179 autoradiobutton,
180 checkbox,
181 combobox,
182 control,
183 ctext,
184 defpushbutton,
185 edittext,
186 hedit,
187 iedit,
188 groupbox,
189 icon,
190 listbox,
191 ltext,
192 pushbox,
193 pushbutton,
194 radiobutton,
195 rtext,
196 scrollbar,
197 state3,
198 userbutton,
199
200 pub const map = std.ComptimeStringMapWithEql(Control, .{
201 .{ "AUTO3STATE", .auto3state },
202 .{ "AUTOCHECKBOX", .autocheckbox },
203 .{ "AUTORADIOBUTTON", .autoradiobutton },
204 .{ "CHECKBOX", .checkbox },
205 .{ "COMBOBOX", .combobox },
206 .{ "CONTROL", .control },
207 .{ "CTEXT", .ctext },
208 .{ "DEFPUSHBUTTON", .defpushbutton },
209 .{ "EDITTEXT", .edittext },
210 .{ "HEDIT", .hedit },
211 .{ "IEDIT", .iedit },
212 .{ "GROUPBOX", .groupbox },
213 .{ "ICON", .icon },
214 .{ "LISTBOX", .listbox },
215 .{ "LTEXT", .ltext },
216 .{ "PUSHBOX", .pushbox },
217 .{ "PUSHBUTTON", .pushbutton },
218 .{ "RADIOBUTTON", .radiobutton },
219 .{ "RTEXT", .rtext },
220 .{ "SCROLLBAR", .scrollbar },
221 .{ "STATE3", .state3 },
222 .{ "USERBUTTON", .userbutton },
223 }, std.comptime_string_map.eqlAsciiIgnoreCase);
224
225 pub fn hasTextParam(control: Control) bool {
226 switch (control) {
227 .scrollbar, .listbox, .iedit, .hedit, .edittext, .combobox => return false,
228 else => return true,
229 }
230 }
231};
232
233pub const ControlClass = struct {
234 pub const map = std.ComptimeStringMapWithEql(res.ControlClass, .{
235 .{ "BUTTON", .button },
236 .{ "EDIT", .edit },
237 .{ "STATIC", .static },
238 .{ "LISTBOX", .listbox },
239 .{ "SCROLLBAR", .scrollbar },
240 .{ "COMBOBOX", .combobox },
241 }, std.comptime_string_map.eqlAsciiIgnoreCase);
242
243 /// Like `map.get` but works on WTF16 strings, for use with parsed
244 /// string literals ("BUTTON", or even "\x42UTTON")
245 pub fn fromWideString(str: []const u16) ?res.ControlClass {
246 const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
247 return if (ascii.eqlIgnoreCaseW(str, utf16Literal("BUTTON")))
248 .button
249 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("EDIT")))
250 .edit
251 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("STATIC")))
252 .static
253 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("LISTBOX")))
254 .listbox
255 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("SCROLLBAR")))
256 .scrollbar
257 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("COMBOBOX")))
258 .combobox
259 else
260 null;
261 }
262};
263
264const ascii = struct {
265 /// Compares ASCII values case-insensitively, non-ASCII values are compared directly
266 pub fn eqlIgnoreCaseW(a: []const u16, b: []const u16) bool {
267 if (a.len != b.len) return false;
268 for (a, b) |a_c, b_c| {
269 if (a_c < 128) {
270 if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false;
271 } else {
272 if (a_c != b_c) return false;
273 }
274 }
275 return true;
276 }
277};
278
279pub const MenuItem = enum {
280 menuitem,
281 popup,
282
283 pub const map = std.ComptimeStringMapWithEql(MenuItem, .{
284 .{ "MENUITEM", .menuitem },
285 .{ "POPUP", .popup },
286 }, std.comptime_string_map.eqlAsciiIgnoreCase);
287
288 pub fn isSeparator(bytes: []const u8) bool {
289 return std.ascii.eqlIgnoreCase(bytes, "SEPARATOR");
290 }
291
292 pub const Option = enum {
293 checked,
294 grayed,
295 help,
296 inactive,
297 menubarbreak,
298 menubreak,
299
300 pub const map = std.ComptimeStringMapWithEql(Option, .{
301 .{ "CHECKED", .checked },
302 .{ "GRAYED", .grayed },
303 .{ "HELP", .help },
304 .{ "INACTIVE", .inactive },
305 .{ "MENUBARBREAK", .menubarbreak },
306 .{ "MENUBREAK", .menubreak },
307 }, std.comptime_string_map.eqlAsciiIgnoreCase);
308 };
309};
310
311pub const ToolbarButton = enum {
312 button,
313 separator,
314
315 pub const map = std.ComptimeStringMapWithEql(ToolbarButton, .{
316 .{ "BUTTON", .button },
317 .{ "SEPARATOR", .separator },
318 }, std.comptime_string_map.eqlAsciiIgnoreCase);
319};
320
321pub const VersionInfo = enum {
322 file_version,
323 product_version,
324 file_flags_mask,
325 file_flags,
326 file_os,
327 file_type,
328 file_subtype,
329
330 pub const map = std.ComptimeStringMapWithEql(VersionInfo, .{
331 .{ "FILEVERSION", .file_version },
332 .{ "PRODUCTVERSION", .product_version },
333 .{ "FILEFLAGSMASK", .file_flags_mask },
334 .{ "FILEFLAGS", .file_flags },
335 .{ "FILEOS", .file_os },
336 .{ "FILETYPE", .file_type },
337 .{ "FILESUBTYPE", .file_subtype },
338 }, std.comptime_string_map.eqlAsciiIgnoreCase);
339};
340
341pub const VersionBlock = enum {
342 block,
343 value,
344
345 pub const map = std.ComptimeStringMapWithEql(VersionBlock, .{
346 .{ "BLOCK", .block },
347 .{ "VALUE", .value },
348 }, std.comptime_string_map.eqlAsciiIgnoreCase);
349};
350
351/// Keywords that are be the first token in a statement and (if so) dictate how the rest
352/// of the statement is parsed.
353pub const TopLevelKeywords = enum {
354 language,
355 version,
356 characteristics,
357 stringtable,
358
359 pub const map = std.ComptimeStringMapWithEql(TopLevelKeywords, .{
360 .{ "LANGUAGE", .language },
361 .{ "VERSION", .version },
362 .{ "CHARACTERISTICS", .characteristics },
363 .{ "STRINGTABLE", .stringtable },
364 }, std.comptime_string_map.eqlAsciiIgnoreCase);
365};
366
367pub const CommonResourceAttributes = enum {
368 preload,
369 loadoncall,
370 fixed,
371 moveable,
372 discardable,
373 pure,
374 impure,
375 shared,
376 nonshared,
377
378 pub const map = std.ComptimeStringMapWithEql(CommonResourceAttributes, .{
379 .{ "PRELOAD", .preload },
380 .{ "LOADONCALL", .loadoncall },
381 .{ "FIXED", .fixed },
382 .{ "MOVEABLE", .moveable },
383 .{ "DISCARDABLE", .discardable },
384 .{ "PURE", .pure },
385 .{ "IMPURE", .impure },
386 .{ "SHARED", .shared },
387 .{ "NONSHARED", .nonshared },
388 }, std.comptime_string_map.eqlAsciiIgnoreCase);
389};
390
391pub const AcceleratorTypeAndOptions = enum {
392 virtkey,
393 ascii,
394 noinvert,
395 alt,
396 shift,
397 control,
398
399 pub const map = std.ComptimeStringMapWithEql(AcceleratorTypeAndOptions, .{
400 .{ "VIRTKEY", .virtkey },
401 .{ "ASCII", .ascii },
402 .{ "NOINVERT", .noinvert },
403 .{ "ALT", .alt },
404 .{ "SHIFT", .shift },
405 .{ "CONTROL", .control },
406 }, std.comptime_string_map.eqlAsciiIgnoreCase);
407};
src/resinator/res.zig deleted-1107
...@@ -1,1107 +0,0 @@
1const std = @import("std");
2const rc = @import("rc.zig");
3const Resource = rc.Resource;
4const CommonResourceAttributes = rc.CommonResourceAttributes;
5const Allocator = std.mem.Allocator;
6const windows1252 = @import("windows1252.zig");
7const CodePage = @import("code_pages.zig").CodePage;
8const literals = @import("literals.zig");
9const SourceBytes = literals.SourceBytes;
10const Codepoint = @import("code_pages.zig").Codepoint;
11const lang = @import("lang.zig");
12const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
13
14/// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
15pub const RT = enum(u8) {
16 ACCELERATOR = 9,
17 ANICURSOR = 21,
18 ANIICON = 22,
19 BITMAP = 2,
20 CURSOR = 1,
21 DIALOG = 5,
22 DLGINCLUDE = 17,
23 DLGINIT = 240,
24 FONT = 8,
25 FONTDIR = 7,
26 GROUP_CURSOR = 1 + 11, // CURSOR + 11
27 GROUP_ICON = 3 + 11, // ICON + 11
28 HTML = 23,
29 ICON = 3,
30 MANIFEST = 24,
31 MENU = 4,
32 MESSAGETABLE = 11,
33 PLUGPLAY = 19,
34 RCDATA = 10,
35 STRING = 6,
36 TOOLBAR = 241,
37 VERSION = 16,
38 VXD = 20,
39 _,
40
41 /// Returns null if the resource type is user-defined
42 /// Asserts that the resource is not `stringtable`
43 pub fn fromResource(resource: Resource) ?RT {
44 return switch (resource) {
45 .accelerators => .ACCELERATOR,
46 .bitmap => .BITMAP,
47 .cursor => .GROUP_CURSOR,
48 .dialog => .DIALOG,
49 .dialogex => .DIALOG,
50 .dlginclude => .DLGINCLUDE,
51 .dlginit => .DLGINIT,
52 .font => .FONT,
53 .html => .HTML,
54 .icon => .GROUP_ICON,
55 .menu => .MENU,
56 .menuex => .MENU,
57 .messagetable => .MESSAGETABLE,
58 .plugplay => .PLUGPLAY,
59 .rcdata => .RCDATA,
60 .stringtable => unreachable,
61 .toolbar => .TOOLBAR,
62 .user_defined => null,
63 .versioninfo => .VERSION,
64 .vxd => .VXD,
65
66 .cursor_num => .CURSOR,
67 .icon_num => .ICON,
68 .string_num => .STRING,
69 .anicursor_num => .ANICURSOR,
70 .aniicon_num => .ANIICON,
71 .fontdir_num => .FONTDIR,
72 .manifest_num => .MANIFEST,
73 };
74 }
75};
76
77/// https://learn.microsoft.com/en-us/windows/win32/menurc/common-resource-attributes
78/// https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader
79pub const MemoryFlags = packed struct(u16) {
80 value: u16,
81
82 pub const MOVEABLE: u16 = 0x10;
83 // TODO: SHARED and PURE seem to be the same thing? Testing seems to confirm this but
84 // would like to find mention of it somewhere.
85 pub const SHARED: u16 = 0x20;
86 pub const PURE: u16 = 0x20;
87 pub const PRELOAD: u16 = 0x40;
88 pub const DISCARDABLE: u16 = 0x1000;
89
90 /// Note: The defaults can have combinations that are not possible to specify within
91 /// an .rc file, as the .rc attributes imply other values (i.e. specifying
92 /// DISCARDABLE always implies MOVEABLE and PURE/SHARED, and yet RT_ICON
93 /// has a default of only MOVEABLE | DISCARDABLE).
94 pub fn defaults(predefined_resource_type: ?RT) MemoryFlags {
95 if (predefined_resource_type == null) {
96 return MemoryFlags{ .value = MOVEABLE | SHARED };
97 } else {
98 return switch (predefined_resource_type.?) {
99 // zig fmt: off
100 .RCDATA, .BITMAP, .HTML, .MANIFEST,
101 .ACCELERATOR, .VERSION, .MESSAGETABLE,
102 .DLGINIT, .TOOLBAR, .PLUGPLAY,
103 .VXD, => MemoryFlags{ .value = MOVEABLE | SHARED },
104
105 .GROUP_ICON, .GROUP_CURSOR,
106 .STRING, .FONT, .DIALOG, .MENU,
107 .DLGINCLUDE, => MemoryFlags{ .value = MOVEABLE | SHARED | DISCARDABLE },
108
109 .ICON, .CURSOR, .ANIICON, .ANICURSOR => MemoryFlags{ .value = MOVEABLE | DISCARDABLE },
110 .FONTDIR => MemoryFlags{ .value = MOVEABLE | PRELOAD },
111 // zig fmt: on
112 // Same as predefined_resource_type == null
113 _ => return MemoryFlags{ .value = MOVEABLE | SHARED },
114 };
115 }
116 }
117
118 pub fn set(self: *MemoryFlags, attribute: CommonResourceAttributes) void {
119 switch (attribute) {
120 .preload => self.value |= PRELOAD,
121 .loadoncall => self.value &= ~PRELOAD,
122 .moveable => self.value |= MOVEABLE,
123 .fixed => self.value &= ~(MOVEABLE | DISCARDABLE),
124 .shared => self.value |= SHARED,
125 .nonshared => self.value &= ~(SHARED | DISCARDABLE),
126 .pure => self.value |= PURE,
127 .impure => self.value &= ~(PURE | DISCARDABLE),
128 .discardable => self.value |= DISCARDABLE | MOVEABLE | PURE,
129 }
130 }
131
132 pub fn setGroup(self: *MemoryFlags, attribute: CommonResourceAttributes, implied_shared_or_pure: bool) void {
133 switch (attribute) {
134 .preload => {
135 self.value |= PRELOAD;
136 if (implied_shared_or_pure) self.value &= ~SHARED;
137 },
138 .loadoncall => {
139 self.value &= ~PRELOAD;
140 if (implied_shared_or_pure) self.value |= SHARED;
141 },
142 else => self.set(attribute),
143 }
144 }
145};
146
147/// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers
148pub const Language = packed struct(u16) {
149 // Note: This is the default no matter what locale the current system is set to,
150 // e.g. even if the system's locale is en-GB, en-US will still be the
151 // default language for resources in the Win32 rc compiler.
152 primary_language_id: u10 = lang.LANG_ENGLISH,
153 sublanguage_id: u6 = lang.SUBLANG_ENGLISH_US,
154
155 /// Default language ID as a u16
156 pub const default: u16 = (Language{}).asInt();
157
158 pub fn fromInt(int: u16) Language {
159 return @bitCast(int);
160 }
161
162 pub fn asInt(self: Language) u16 {
163 return @bitCast(self);
164 }
165};
166
167/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks
168pub const ControlClass = enum(u16) {
169 button = 0x80,
170 edit = 0x81,
171 static = 0x82,
172 listbox = 0x83,
173 scrollbar = 0x84,
174 combobox = 0x85,
175
176 pub fn fromControl(control: rc.Control) ?ControlClass {
177 return switch (control) {
178 // zig fmt: off
179 .auto3state, .autocheckbox, .autoradiobutton,
180 .checkbox, .defpushbutton, .groupbox, .pushbox,
181 .pushbutton, .radiobutton, .state3, .userbutton => .button,
182 // zig fmt: on
183 .combobox => .combobox,
184 .control => null,
185 .ctext, .icon, .ltext, .rtext => .static,
186 .edittext, .hedit, .iedit => .edit,
187 .listbox => .listbox,
188 .scrollbar => .scrollbar,
189 };
190 }
191
192 pub fn getImpliedStyle(control: rc.Control) u32 {
193 var style = WS.CHILD | WS.VISIBLE;
194 switch (control) {
195 .auto3state => style |= BS.AUTO3STATE | WS.TABSTOP,
196 .autocheckbox => style |= BS.AUTOCHECKBOX | WS.TABSTOP,
197 .autoradiobutton => style |= BS.AUTORADIOBUTTON,
198 .checkbox => style |= BS.CHECKBOX | WS.TABSTOP,
199 .combobox => {},
200 .control => {},
201 .ctext => style |= SS.CENTER | WS.GROUP,
202 .defpushbutton => style |= BS.DEFPUSHBUTTON | WS.TABSTOP,
203 .edittext, .hedit, .iedit => style |= WS.TABSTOP | WS.BORDER,
204 .groupbox => style |= BS.GROUPBOX,
205 .icon => style |= SS.ICON,
206 .listbox => style |= LBS.NOTIFY | WS.BORDER,
207 .ltext => style |= WS.GROUP,
208 .pushbox => style |= BS.PUSHBOX | WS.TABSTOP,
209 .pushbutton => style |= WS.TABSTOP,
210 .radiobutton => style |= BS.RADIOBUTTON,
211 .rtext => style |= SS.RIGHT | WS.GROUP,
212 .scrollbar => {},
213 .state3 => style |= BS.@"3STATE" | WS.TABSTOP,
214 .userbutton => style |= BS.USERBUTTON | WS.TABSTOP,
215 }
216 return style;
217 }
218};
219
220pub const NameOrOrdinal = union(enum) {
221 // UTF-16 LE
222 name: [:0]const u16,
223 ordinal: u16,
224
225 pub fn deinit(self: NameOrOrdinal, allocator: Allocator) void {
226 switch (self) {
227 .name => |name| {
228 allocator.free(name);
229 },
230 .ordinal => {},
231 }
232 }
233
234 /// Returns the full length of the amount of bytes that would be written by `write`
235 /// (e.g. for an ordinal it will return the length including the 0xFFFF indicator)
236 pub fn byteLen(self: NameOrOrdinal) usize {
237 switch (self) {
238 .name => |name| {
239 // + 1 for 0-terminated
240 return (name.len + 1) * @sizeOf(u16);
241 },
242 .ordinal => return 4,
243 }
244 }
245
246 pub fn write(self: NameOrOrdinal, writer: anytype) !void {
247 switch (self) {
248 .name => |name| {
249 try writer.writeAll(std.mem.sliceAsBytes(name[0 .. name.len + 1]));
250 },
251 .ordinal => |ordinal| {
252 try writer.writeInt(u16, 0xffff, .little);
253 try writer.writeInt(u16, ordinal, .little);
254 },
255 }
256 }
257
258 pub fn writeEmpty(writer: anytype) !void {
259 try writer.writeInt(u16, 0, .little);
260 }
261
262 pub fn fromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
263 if (maybeOrdinalFromString(bytes)) |ordinal| {
264 return ordinal;
265 }
266 return nameFromString(allocator, bytes);
267 }
268
269 pub fn nameFromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
270 // Names have a limit of 256 UTF-16 code units + null terminator
271 var buf = try std.ArrayList(u16).initCapacity(allocator, @min(257, bytes.slice.len));
272 errdefer buf.deinit();
273
274 var i: usize = 0;
275 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
276 if (buf.items.len == 256) break;
277
278 const c = codepoint.value;
279 if (c == Codepoint.invalid) {
280 try buf.append(std.mem.nativeToLittle(u16, '�'));
281 } else if (c < 0x7F) {
282 // ASCII chars in names are always converted to uppercase
283 try buf.append(std.mem.nativeToLittle(u16, std.ascii.toUpper(@intCast(c))));
284 } else if (c < 0x10000) {
285 const short: u16 = @intCast(c);
286 try buf.append(std.mem.nativeToLittle(u16, short));
287 } else {
288 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
289 try buf.append(std.mem.nativeToLittle(u16, high));
290
291 // Note: This can cut-off in the middle of a UTF-16 surrogate pair,
292 // i.e. it can make the string end with an unpaired high surrogate
293 if (buf.items.len == 256) break;
294
295 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
296 try buf.append(std.mem.nativeToLittle(u16, low));
297 }
298 }
299
300 return NameOrOrdinal{ .name = try buf.toOwnedSliceSentinel(0) };
301 }
302
303 /// Returns `null` if the bytes do not form a valid number.
304 /// Does not allow non-ASCII digits (which the Win32 RC compiler does allow
305 /// in base 10 numbers, see `maybeNonAsciiOrdinalFromString`).
306 pub fn maybeOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
307 var buf = bytes.slice;
308 var radix: u8 = 10;
309 if (buf.len > 2 and buf[0] == '0') {
310 switch (buf[1]) {
311 '0'...'9' => {},
312 'x', 'X' => {
313 radix = 16;
314 buf = buf[2..];
315 // only the first 4 hex digits matter, anything else is ignored
316 // i.e. 0x12345 is treated as if it were 0x1234
317 buf.len = @min(buf.len, 4);
318 },
319 else => return null,
320 }
321 }
322
323 var i: usize = 0;
324 var result: u16 = 0;
325 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
326 const c = codepoint.value;
327 const digit: u8 = switch (c) {
328 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch switch (radix) {
329 10 => return null,
330 // non-hex-digits are treated as a terminator rather than invalidating
331 // the number (note: if there are no valid hex digits then the result
332 // will be zero which is not treated as a valid number)
333 16 => break,
334 else => unreachable,
335 },
336 else => if (radix == 10) return null else break,
337 };
338
339 if (result != 0) {
340 result *%= radix;
341 }
342 result +%= digit;
343 }
344
345 // Anything that resolves to zero is not interpretted as a number
346 if (result == 0) return null;
347 return NameOrOrdinal{ .ordinal = result };
348 }
349
350 /// The Win32 RC compiler uses `iswdigit` for digit detection for base 10
351 /// numbers, which means that non-ASCII digits are 'accepted' but handled
352 /// in a totally unintuitive manner, leading to arbitrary results.
353 ///
354 /// This function will return the value that such an ordinal 'would' have
355 /// if it was run through the Win32 RC compiler. This allows us to disallow
356 /// non-ASCII digits in number literals but still detect when the Win32
357 /// RC compiler would have allowed them, so that a proper warning/error
358 /// can be emitted.
359 pub fn maybeNonAsciiOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
360 const buf = bytes.slice;
361 const radix = 10;
362 if (buf.len > 2 and buf[0] == '0') {
363 switch (buf[1]) {
364 // We only care about base 10 numbers here
365 'x', 'X' => return null,
366 else => {},
367 }
368 }
369
370 var i: usize = 0;
371 var result: u16 = 0;
372 while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
373 const c = codepoint.value;
374 const digit: u16 = digit: {
375 const is_digit = (c >= '0' and c <= '9') or isNonAsciiDigit(c);
376 if (!is_digit) return null;
377 break :digit @intCast(c - '0');
378 };
379
380 if (result != 0) {
381 result *%= radix;
382 }
383 result +%= digit;
384 }
385
386 // Anything that resolves to zero is not interpretted as a number
387 if (result == 0) return null;
388 return NameOrOrdinal{ .ordinal = result };
389 }
390
391 pub fn predefinedResourceType(self: NameOrOrdinal) ?RT {
392 switch (self) {
393 .ordinal => |ordinal| {
394 if (ordinal >= 256) return null;
395 switch (@as(RT, @enumFromInt(ordinal))) {
396 .ACCELERATOR,
397 .ANICURSOR,
398 .ANIICON,
399 .BITMAP,
400 .CURSOR,
401 .DIALOG,
402 .DLGINCLUDE,
403 .DLGINIT,
404 .FONT,
405 .FONTDIR,
406 .GROUP_CURSOR,
407 .GROUP_ICON,
408 .HTML,
409 .ICON,
410 .MANIFEST,
411 .MENU,
412 .MESSAGETABLE,
413 .PLUGPLAY,
414 .RCDATA,
415 .STRING,
416 .TOOLBAR,
417 .VERSION,
418 .VXD,
419 => |rt| return rt,
420 _ => return null,
421 }
422 },
423 .name => return null,
424 }
425 }
426};
427
428fn expectNameOrOrdinal(expected: NameOrOrdinal, actual: NameOrOrdinal) !void {
429 switch (expected) {
430 .name => {
431 if (actual != .name) return error.TestExpectedEqual;
432 try std.testing.expectEqualSlices(u16, expected.name, actual.name);
433 },
434 .ordinal => {
435 if (actual != .ordinal) return error.TestExpectedEqual;
436 try std.testing.expectEqual(expected.ordinal, actual.ordinal);
437 },
438 }
439}
440
441test "NameOrOrdinal" {
442 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
443 defer arena.deinit();
444
445 const allocator = arena.allocator();
446
447 // zero is treated as a string
448 try expectNameOrOrdinal(
449 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0") },
450 try NameOrOrdinal.fromString(allocator, .{ .slice = "0", .code_page = .windows1252 }),
451 );
452 // any non-digit byte invalidates the number
453 try expectNameOrOrdinal(
454 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1A") },
455 try NameOrOrdinal.fromString(allocator, .{ .slice = "1a", .code_page = .windows1252 }),
456 );
457 try expectNameOrOrdinal(
458 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1ÿ") },
459 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\xff", .code_page = .windows1252 }),
460 );
461 try expectNameOrOrdinal(
462 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1€") },
463 try NameOrOrdinal.fromString(allocator, .{ .slice = "1€", .code_page = .utf8 }),
464 );
465 try expectNameOrOrdinal(
466 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1�") },
467 try NameOrOrdinal.fromString(allocator, .{ .slice = "1\x80", .code_page = .utf8 }),
468 );
469 // same with overflow that resolves to 0
470 try expectNameOrOrdinal(
471 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("65536") },
472 try NameOrOrdinal.fromString(allocator, .{ .slice = "65536", .code_page = .windows1252 }),
473 );
474 // hex zero is also treated as a string
475 try expectNameOrOrdinal(
476 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0X0") },
477 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x0", .code_page = .windows1252 }),
478 );
479 // hex numbers work
480 try expectNameOrOrdinal(
481 NameOrOrdinal{ .ordinal = 0x100 },
482 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x100", .code_page = .windows1252 }),
483 );
484 // only the first 4 hex digits matter
485 try expectNameOrOrdinal(
486 NameOrOrdinal{ .ordinal = 0x1234 },
487 try NameOrOrdinal.fromString(allocator, .{ .slice = "0X12345", .code_page = .windows1252 }),
488 );
489 // octal is not supported so it gets treated as a string
490 try expectNameOrOrdinal(
491 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0O1234") },
492 try NameOrOrdinal.fromString(allocator, .{ .slice = "0o1234", .code_page = .windows1252 }),
493 );
494 // overflow wraps
495 try expectNameOrOrdinal(
496 NameOrOrdinal{ .ordinal = @truncate(65635) },
497 try NameOrOrdinal.fromString(allocator, .{ .slice = "65635", .code_page = .windows1252 }),
498 );
499 // non-hex-digits in a hex literal are treated as a terminator
500 try expectNameOrOrdinal(
501 NameOrOrdinal{ .ordinal = 0x4 },
502 try NameOrOrdinal.fromString(allocator, .{ .slice = "0x4n", .code_page = .windows1252 }),
503 );
504 try expectNameOrOrdinal(
505 NameOrOrdinal{ .ordinal = 0xFA },
506 try NameOrOrdinal.fromString(allocator, .{ .slice = "0xFAZ92348", .code_page = .windows1252 }),
507 );
508 // 0 at the start is allowed
509 try expectNameOrOrdinal(
510 NameOrOrdinal{ .ordinal = 50 },
511 try NameOrOrdinal.fromString(allocator, .{ .slice = "050", .code_page = .windows1252 }),
512 );
513 // limit of 256 UTF-16 code units, can cut off between a surrogate pair
514 {
515 var expected = blk: {
516 // the input before the 𐐷 character, but uppercased
517 const expected_u8_bytes = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528QFFL7SHNSIETG0QKLR1UYPBTUV1PMFQRRA0VJDG354GQEDJMUPGPP1W1EXVNTZVEIZ6K3IPQM1AWGEYALMEODYVEZGOD3MFMGEY8FNR4JUETTB1PZDEWSNDRGZUA8SNXP3NGO";
518 var buf: [256:0]u16 = undefined;
519 for (expected_u8_bytes, 0..) |byte, i| {
520 buf[i] = std.mem.nativeToLittle(u16, byte);
521 }
522 // surrogate pair that is now orphaned
523 buf[255] = std.mem.nativeToLittle(u16, 0xD801);
524 break :blk buf;
525 };
526 try expectNameOrOrdinal(
527 NameOrOrdinal{ .name = &expected },
528 try NameOrOrdinal.fromString(allocator, .{
529 .slice = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528qffL7ShnSIETg0qkLr1UYpbtuv1PMFQRRa0VjDG354GQedJmUPgpp1w1ExVnTzVEiz6K3iPqM1AWGeYALmeODyvEZGOD3MfmGey8fnR4jUeTtB1PzdeWsNDrGzuA8Snxp3NGO𐐷",
530 .code_page = .utf8,
531 }),
532 );
533 }
534}
535
536test "NameOrOrdinal code page awareness" {
537 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
538 defer arena.deinit();
539
540 const allocator = arena.allocator();
541
542 try expectNameOrOrdinal(
543 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("��𐐷") },
544 try NameOrOrdinal.fromString(allocator, .{
545 .slice = "\xF0\x80\x80𐐷",
546 .code_page = .utf8,
547 }),
548 );
549 try expectNameOrOrdinal(
550 // The UTF-8 representation of 𐐷 is 0xF0 0x90 0x90 0xB7. In order to provide valid
551 // UTF-8 to utf8ToUtf16LeStringLiteral, it uses the UTF-8 representation of the codepoint
552 // <U+0x90> which is 0xC2 0x90. The code units in the expected UTF-16 string are:
553 // { 0x00F0, 0x20AC, 0x20AC, 0x00F0, 0x0090, 0x0090, 0x00B7 }
554 NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("ð€€ð\xC2\x90\xC2\x90·") },
555 try NameOrOrdinal.fromString(allocator, .{
556 .slice = "\xF0\x80\x80𐐷",
557 .code_page = .windows1252,
558 }),
559 );
560}
561
562/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-accel#members
563/// https://devblogs.microsoft.com/oldnewthing/20070316-00/?p=27593
564pub const AcceleratorModifiers = struct {
565 value: u8 = 0,
566 explicit_ascii_or_virtkey: bool = false,
567
568 pub const ASCII = 0;
569 pub const VIRTKEY = 1;
570 pub const NOINVERT = 1 << 1;
571 pub const SHIFT = 1 << 2;
572 pub const CONTROL = 1 << 3;
573 pub const ALT = 1 << 4;
574 /// Marker for the last accelerator in an accelerator table
575 pub const last_accelerator_in_table = 1 << 7;
576
577 pub fn apply(self: *AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) void {
578 if (modifier == .ascii or modifier == .virtkey) self.explicit_ascii_or_virtkey = true;
579 self.value |= modifierValue(modifier);
580 }
581
582 pub fn isSet(self: AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) bool {
583 // ASCII is set whenever VIRTKEY is not
584 if (modifier == .ascii) return self.value & modifierValue(.virtkey) == 0;
585 return self.value & modifierValue(modifier) != 0;
586 }
587
588 fn modifierValue(modifier: rc.AcceleratorTypeAndOptions) u8 {
589 return switch (modifier) {
590 .ascii => ASCII,
591 .virtkey => VIRTKEY,
592 .noinvert => NOINVERT,
593 .shift => SHIFT,
594 .control => CONTROL,
595 .alt => ALT,
596 };
597 }
598
599 pub fn markLast(self: *AcceleratorModifiers) void {
600 self.value |= last_accelerator_in_table;
601 }
602};
603
604const AcceleratorKeyCodepointTranslator = struct {
605 string_type: literals.StringType,
606
607 pub fn translate(self: @This(), maybe_parsed: ?literals.IterativeStringParser.ParsedCodepoint) ?u21 {
608 const parsed = maybe_parsed orelse return null;
609 if (parsed.codepoint == Codepoint.invalid) return 0xFFFD;
610 if (parsed.from_escaped_integer and self.string_type == .ascii) {
611 return windows1252.toCodepoint(@intCast(parsed.codepoint));
612 }
613 return parsed.codepoint;
614 }
615};
616
617pub const ParseAcceleratorKeyStringError = error{ EmptyAccelerator, AcceleratorTooLong, InvalidControlCharacter, ControlCharacterOutOfRange };
618
619/// Expects bytes to be the full bytes of a string literal token (e.g. including the "" or L"").
620pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: literals.StringParseOptions) (ParseAcceleratorKeyStringError || Allocator.Error)!u16 {
621 if (bytes.slice.len == 0) {
622 return error.EmptyAccelerator;
623 }
624
625 var parser = literals.IterativeStringParser.init(bytes, options);
626 var translator = AcceleratorKeyCodepointTranslator{ .string_type = parser.declared_string_type };
627
628 const first_codepoint = translator.translate(try parser.next()) orelse return error.EmptyAccelerator;
629 // 0 is treated as a terminator, so this is equivalent to an empty string
630 if (first_codepoint == 0) return error.EmptyAccelerator;
631
632 if (first_codepoint == '^') {
633 // Note: Emitting this warning unconditonally whenever ^ is the first character
634 // matches the Win32 RC behavior, but it's questionable whether or not
635 // the warning should be emitted for ^^ since that results in the ASCII
636 // character ^ being written to the .res.
637 if (is_virt and options.diagnostics != null) {
638 try options.diagnostics.?.diagnostics.append(.{
639 .err = .ascii_character_not_equivalent_to_virtual_key_code,
640 .type = .warning,
641 .token = options.diagnostics.?.token,
642 });
643 }
644
645 const c = translator.translate(try parser.next()) orelse return error.InvalidControlCharacter;
646 switch (c) {
647 '^' => return '^', // special case
648 'a'...'z', 'A'...'Z' => return std.ascii.toUpper(@intCast(c)) - 0x40,
649 // Note: The Windows RC compiler allows more than just A-Z, but what it allows
650 // seems to be tied to some sort of Unicode-aware 'is character' function or something.
651 // The full list of codepoints that trigger an out-of-range error can be found here:
652 // https://gist.github.com/squeek502/2e9d0a4728a83eed074ad9785a209fd0
653 // For codepoints >= 0x80 that don't trigger the error, the Windows RC compiler takes the
654 // codepoint and does the `- 0x40` transformation as if it were A-Z which couldn't lead
655 // to anything useable, so there's no point in emulating that behavior--erroring for
656 // all non-[a-zA-Z] makes much more sense and is what was probably intended by the
657 // Windows RC compiler.
658 else => return error.ControlCharacterOutOfRange,
659 }
660 @compileError("this should be unreachable");
661 }
662
663 const second_codepoint = translator.translate(try parser.next());
664
665 var result: u32 = initial_value: {
666 if (first_codepoint >= 0x10000) {
667 if (second_codepoint != null and second_codepoint.? != 0) return error.AcceleratorTooLong;
668 // No idea why it works this way, but this seems to match the Windows RC
669 // behavior for codepoints >= 0x10000
670 const low = @as(u16, @intCast(first_codepoint & 0x3FF)) + 0xDC00;
671 const extra = (first_codepoint - 0x10000) / 0x400;
672 break :initial_value low + extra * 0x100;
673 }
674 break :initial_value first_codepoint;
675 };
676
677 // 0 is treated as a terminator
678 if (second_codepoint != null and second_codepoint.? == 0) return @truncate(result);
679
680 const third_codepoint = translator.translate(try parser.next());
681 // 0 is treated as a terminator, so a 0 in the third position is fine but
682 // anything else is too many codepoints for an accelerator
683 if (third_codepoint != null and third_codepoint.? != 0) return error.AcceleratorTooLong;
684
685 if (second_codepoint) |c| {
686 if (c >= 0x10000) return error.AcceleratorTooLong;
687 result <<= 8;
688 result += c;
689 } else if (is_virt) {
690 switch (result) {
691 'a'...'z' => result -= 0x20, // toUpper
692 else => {},
693 }
694 }
695 return @truncate(result);
696}
697
698test "accelerator keys" {
699 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
700 .{ .slice = "\"^a\"", .code_page = .windows1252 },
701 false,
702 .{},
703 ));
704 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
705 .{ .slice = "\"^A\"", .code_page = .windows1252 },
706 false,
707 .{},
708 ));
709 try std.testing.expectEqual(@as(u16, 26), try parseAcceleratorKeyString(
710 .{ .slice = "\"^Z\"", .code_page = .windows1252 },
711 false,
712 .{},
713 ));
714 try std.testing.expectEqual(@as(u16, '^'), try parseAcceleratorKeyString(
715 .{ .slice = "\"^^\"", .code_page = .windows1252 },
716 false,
717 .{},
718 ));
719
720 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
721 .{ .slice = "\"a\"", .code_page = .windows1252 },
722 false,
723 .{},
724 ));
725 try std.testing.expectEqual(@as(u16, 0x6162), try parseAcceleratorKeyString(
726 .{ .slice = "\"ab\"", .code_page = .windows1252 },
727 false,
728 .{},
729 ));
730
731 try std.testing.expectEqual(@as(u16, 'C'), try parseAcceleratorKeyString(
732 .{ .slice = "\"c\"", .code_page = .windows1252 },
733 true,
734 .{},
735 ));
736 try std.testing.expectEqual(@as(u16, 0x6363), try parseAcceleratorKeyString(
737 .{ .slice = "\"cc\"", .code_page = .windows1252 },
738 true,
739 .{},
740 ));
741
742 // \x00 or any escape that evaluates to zero acts as a terminator, everything past it
743 // is ignored
744 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
745 .{ .slice = "\"a\\0bcdef\"", .code_page = .windows1252 },
746 false,
747 .{},
748 ));
749
750 // \x80 is € in Windows-1252, which is Unicode codepoint 20AC
751 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
752 .{ .slice = "\"\x80\"", .code_page = .windows1252 },
753 false,
754 .{},
755 ));
756 // This depends on the code page, though, with codepage 65001, \x80
757 // on its own is invalid UTF-8 so it gets converted to the replacement character
758 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
759 .{ .slice = "\"\x80\"", .code_page = .utf8 },
760 false,
761 .{},
762 ));
763 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
764 .{ .slice = "\"\x80\x80\"", .code_page = .windows1252 },
765 false,
766 .{},
767 ));
768 // This also behaves the same with escaped characters
769 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
770 .{ .slice = "\"\\x80\"", .code_page = .windows1252 },
771 false,
772 .{},
773 ));
774 // Even with utf8 code page
775 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
776 .{ .slice = "\"\\x80\"", .code_page = .utf8 },
777 false,
778 .{},
779 ));
780 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
781 .{ .slice = "\"\\x80\\x80\"", .code_page = .windows1252 },
782 false,
783 .{},
784 ));
785 // Wide string with the actual characters behaves like the ASCII string version
786 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
787 .{ .slice = "L\"\x80\x80\"", .code_page = .windows1252 },
788 false,
789 .{},
790 ));
791 // But wide string with escapes behaves differently
792 try std.testing.expectEqual(@as(u16, 0x8080), try parseAcceleratorKeyString(
793 .{ .slice = "L\"\\x80\\x80\"", .code_page = .windows1252 },
794 false,
795 .{},
796 ));
797 // and invalid escapes within wide strings get skipped
798 try std.testing.expectEqual(@as(u16, 'z'), try parseAcceleratorKeyString(
799 .{ .slice = "L\"\\Hz\"", .code_page = .windows1252 },
800 false,
801 .{},
802 ));
803
804 // any non-A-Z codepoints are illegal
805 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
806 .{ .slice = "\"^\x83\"", .code_page = .windows1252 },
807 false,
808 .{},
809 ));
810 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
811 .{ .slice = "\"^1\"", .code_page = .windows1252 },
812 false,
813 .{},
814 ));
815 try std.testing.expectError(error.InvalidControlCharacter, parseAcceleratorKeyString(
816 .{ .slice = "\"^\"", .code_page = .windows1252 },
817 false,
818 .{},
819 ));
820 try std.testing.expectError(error.EmptyAccelerator, parseAcceleratorKeyString(
821 .{ .slice = "\"\"", .code_page = .windows1252 },
822 false,
823 .{},
824 ));
825 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
826 .{ .slice = "\"hello\"", .code_page = .windows1252 },
827 false,
828 .{},
829 ));
830 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
831 .{ .slice = "\"^\x80\"", .code_page = .windows1252 },
832 false,
833 .{},
834 ));
835
836 // Invalid UTF-8 gets converted to 0xFFFD, multiple invalids get shifted and added together
837 // The behavior is the same for ascii and wide strings
838 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
839 .{ .slice = "\"\x80\x80\"", .code_page = .utf8 },
840 false,
841 .{},
842 ));
843 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
844 .{ .slice = "L\"\x80\x80\"", .code_page = .utf8 },
845 false,
846 .{},
847 ));
848
849 // Codepoints >= 0x10000
850 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
851 .{ .slice = "\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
852 false,
853 .{},
854 ));
855 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
856 .{ .slice = "L\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
857 false,
858 .{},
859 ));
860 try std.testing.expectEqual(@as(u16, 0x9C01), try parseAcceleratorKeyString(
861 .{ .slice = "\"\xF4\x80\x80\x81\"", .code_page = .utf8 },
862 false,
863 .{},
864 ));
865 // anything before or after a codepoint >= 0x10000 causes an error
866 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
867 .{ .slice = "\"a\xF0\x90\x80\x80\"", .code_page = .utf8 },
868 false,
869 .{},
870 ));
871 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
872 .{ .slice = "\"\xF0\x90\x80\x80a\"", .code_page = .utf8 },
873 false,
874 .{},
875 ));
876}
877
878pub const ForcedOrdinal = struct {
879 pub fn fromBytes(bytes: SourceBytes) u16 {
880 var i: usize = 0;
881 var result: u21 = 0;
882 while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
883 const c = switch (codepoint.value) {
884 // Codepoints that would need a surrogate pair in UTF-16 are
885 // broken up into their UTF-16 code units and each code unit
886 // is interpreted as a digit.
887 0x10000...0x10FFFF => {
888 const high = @as(u16, @intCast((codepoint.value - 0x10000) >> 10)) + 0xD800;
889 if (result != 0) result *%= 10;
890 result +%= high -% '0';
891
892 const low = @as(u16, @intCast(codepoint.value & 0x3FF)) + 0xDC00;
893 if (result != 0) result *%= 10;
894 result +%= low -% '0';
895 continue;
896 },
897 Codepoint.invalid => 0xFFFD,
898 else => codepoint.value,
899 };
900 if (result != 0) result *%= 10;
901 result +%= c -% '0';
902 }
903 return @truncate(result);
904 }
905
906 pub fn fromUtf16Le(utf16: [:0]const u16) u16 {
907 var result: u16 = 0;
908 for (utf16) |code_unit| {
909 if (result != 0) result *%= 10;
910 result +%= std.mem.littleToNative(u16, code_unit) -% '0';
911 }
912 return result;
913 }
914};
915
916test "forced ordinal" {
917 try std.testing.expectEqual(@as(u16, 3200), ForcedOrdinal.fromBytes(.{ .slice = "3200", .code_page = .windows1252 }));
918 try std.testing.expectEqual(@as(u16, 0x33), ForcedOrdinal.fromBytes(.{ .slice = "1+1", .code_page = .windows1252 }));
919 try std.testing.expectEqual(@as(u16, 65531), ForcedOrdinal.fromBytes(.{ .slice = "1!", .code_page = .windows1252 }));
920
921 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0\x8C", .code_page = .windows1252 }));
922 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0Œ", .code_page = .utf8 }));
923
924 // invalid UTF-8 gets converted to 0xFFFD (replacement char) and then interpreted as a digit
925 try std.testing.expectEqual(@as(u16, 0xFFCD), ForcedOrdinal.fromBytes(.{ .slice = "0\x81", .code_page = .utf8 }));
926 // codepoints >= 0x10000
927 try std.testing.expectEqual(@as(u16, 0x49F2), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10002}", .code_page = .utf8 }));
928 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10100}", .code_page = .utf8 }));
929
930 // From UTF-16
931 try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromUtf16Le(&[_:0]u16{ std.mem.nativeToLittle(u16, '0'), std.mem.nativeToLittle(u16, 'Œ') }));
932 try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromUtf16Le(std.unicode.utf8ToUtf16LeStringLiteral("0\u{10100}")));
933}
934
935/// https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
936pub const FixedFileInfo = struct {
937 file_version: Version = .{},
938 product_version: Version = .{},
939 file_flags_mask: u32 = 0,
940 file_flags: u32 = 0,
941 file_os: u32 = 0,
942 file_type: u32 = 0,
943 file_subtype: u32 = 0,
944 file_date: Version = .{}, // TODO: I think this is always all zeroes?
945
946 pub const signature = 0xFEEF04BD;
947 // Note: This corresponds to a version of 1.0
948 pub const version = 0x00010000;
949
950 pub const byte_len = 0x34;
951 pub const key = std.unicode.utf8ToUtf16LeStringLiteral("VS_VERSION_INFO");
952
953 pub const Version = struct {
954 parts: [4]u16 = [_]u16{0} ** 4,
955
956 pub fn mostSignificantCombinedParts(self: Version) u32 {
957 return (@as(u32, self.parts[0]) << 16) + self.parts[1];
958 }
959
960 pub fn leastSignificantCombinedParts(self: Version) u32 {
961 return (@as(u32, self.parts[2]) << 16) + self.parts[3];
962 }
963 };
964
965 pub fn write(self: FixedFileInfo, writer: anytype) !void {
966 try writer.writeInt(u32, signature, .little);
967 try writer.writeInt(u32, version, .little);
968 try writer.writeInt(u32, self.file_version.mostSignificantCombinedParts(), .little);
969 try writer.writeInt(u32, self.file_version.leastSignificantCombinedParts(), .little);
970 try writer.writeInt(u32, self.product_version.mostSignificantCombinedParts(), .little);
971 try writer.writeInt(u32, self.product_version.leastSignificantCombinedParts(), .little);
972 try writer.writeInt(u32, self.file_flags_mask, .little);
973 try writer.writeInt(u32, self.file_flags, .little);
974 try writer.writeInt(u32, self.file_os, .little);
975 try writer.writeInt(u32, self.file_type, .little);
976 try writer.writeInt(u32, self.file_subtype, .little);
977 try writer.writeInt(u32, self.file_date.mostSignificantCombinedParts(), .little);
978 try writer.writeInt(u32, self.file_date.leastSignificantCombinedParts(), .little);
979 }
980};
981
982test "FixedFileInfo.Version" {
983 const version = FixedFileInfo.Version{
984 .parts = .{ 1, 2, 3, 4 },
985 };
986 try std.testing.expectEqual(@as(u32, 0x00010002), version.mostSignificantCombinedParts());
987 try std.testing.expectEqual(@as(u32, 0x00030004), version.leastSignificantCombinedParts());
988}
989
990pub const VersionNode = struct {
991 pub const type_string: u16 = 1;
992 pub const type_binary: u16 = 0;
993};
994
995pub const MenuItemFlags = struct {
996 value: u16 = 0,
997
998 pub fn apply(self: *MenuItemFlags, option: rc.MenuItem.Option) void {
999 self.value |= optionValue(option);
1000 }
1001
1002 pub fn isSet(self: MenuItemFlags, option: rc.MenuItem.Option) bool {
1003 return self.value & optionValue(option) != 0;
1004 }
1005
1006 fn optionValue(option: rc.MenuItem.Option) u16 {
1007 return @intCast(switch (option) {
1008 .checked => MF.CHECKED,
1009 .grayed => MF.GRAYED,
1010 .help => MF.HELP,
1011 .inactive => MF.DISABLED,
1012 .menubarbreak => MF.MENUBARBREAK,
1013 .menubreak => MF.MENUBREAK,
1014 });
1015 }
1016
1017 pub fn markLast(self: *MenuItemFlags) void {
1018 self.value |= @intCast(MF.END);
1019 }
1020};
1021
1022/// Menu Flags from WinUser.h
1023/// This is not complete, it only contains what is needed
1024pub const MF = struct {
1025 pub const GRAYED: u32 = 0x00000001;
1026 pub const DISABLED: u32 = 0x00000002;
1027 pub const CHECKED: u32 = 0x00000008;
1028 pub const POPUP: u32 = 0x00000010;
1029 pub const MENUBARBREAK: u32 = 0x00000020;
1030 pub const MENUBREAK: u32 = 0x00000040;
1031 pub const HELP: u32 = 0x00004000;
1032 pub const END: u32 = 0x00000080;
1033};
1034
1035/// Window Styles from WinUser.h
1036pub const WS = struct {
1037 pub const OVERLAPPED: u32 = 0x00000000;
1038 pub const POPUP: u32 = 0x80000000;
1039 pub const CHILD: u32 = 0x40000000;
1040 pub const MINIMIZE: u32 = 0x20000000;
1041 pub const VISIBLE: u32 = 0x10000000;
1042 pub const DISABLED: u32 = 0x08000000;
1043 pub const CLIPSIBLINGS: u32 = 0x04000000;
1044 pub const CLIPCHILDREN: u32 = 0x02000000;
1045 pub const MAXIMIZE: u32 = 0x01000000;
1046 pub const CAPTION: u32 = BORDER | DLGFRAME;
1047 pub const BORDER: u32 = 0x00800000;
1048 pub const DLGFRAME: u32 = 0x00400000;
1049 pub const VSCROLL: u32 = 0x00200000;
1050 pub const HSCROLL: u32 = 0x00100000;
1051 pub const SYSMENU: u32 = 0x00080000;
1052 pub const THICKFRAME: u32 = 0x00040000;
1053 pub const GROUP: u32 = 0x00020000;
1054 pub const TABSTOP: u32 = 0x00010000;
1055
1056 pub const MINIMIZEBOX: u32 = 0x00020000;
1057 pub const MAXIMIZEBOX: u32 = 0x00010000;
1058
1059 pub const TILED: u32 = OVERLAPPED;
1060 pub const ICONIC: u32 = MINIMIZE;
1061 pub const SIZEBOX: u32 = THICKFRAME;
1062 pub const TILEDWINDOW: u32 = OVERLAPPEDWINDOW;
1063
1064 // Common Window Styles
1065 pub const OVERLAPPEDWINDOW: u32 = OVERLAPPED | CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX;
1066 pub const POPUPWINDOW: u32 = POPUP | BORDER | SYSMENU;
1067 pub const CHILDWINDOW: u32 = CHILD;
1068};
1069
1070/// Dialog Box Template Styles from WinUser.h
1071pub const DS = struct {
1072 pub const SETFONT: u32 = 0x40;
1073};
1074
1075/// Button Control Styles from WinUser.h
1076/// This is not complete, it only contains what is needed
1077pub const BS = struct {
1078 pub const PUSHBUTTON: u32 = 0x00000000;
1079 pub const DEFPUSHBUTTON: u32 = 0x00000001;
1080 pub const CHECKBOX: u32 = 0x00000002;
1081 pub const AUTOCHECKBOX: u32 = 0x00000003;
1082 pub const RADIOBUTTON: u32 = 0x00000004;
1083 pub const @"3STATE": u32 = 0x00000005;
1084 pub const AUTO3STATE: u32 = 0x00000006;
1085 pub const GROUPBOX: u32 = 0x00000007;
1086 pub const USERBUTTON: u32 = 0x00000008;
1087 pub const AUTORADIOBUTTON: u32 = 0x00000009;
1088 pub const PUSHBOX: u32 = 0x0000000A;
1089 pub const OWNERDRAW: u32 = 0x0000000B;
1090 pub const TYPEMASK: u32 = 0x0000000F;
1091 pub const LEFTTEXT: u32 = 0x00000020;
1092};
1093
1094/// Static Control Constants from WinUser.h
1095/// This is not complete, it only contains what is needed
1096pub const SS = struct {
1097 pub const LEFT: u32 = 0x00000000;
1098 pub const CENTER: u32 = 0x00000001;
1099 pub const RIGHT: u32 = 0x00000002;
1100 pub const ICON: u32 = 0x00000003;
1101};
1102
1103/// Listbox Styles from WinUser.h
1104/// This is not complete, it only contains what is needed
1105pub const LBS = struct {
1106 pub const NOTIFY: u32 = 0x0001;
1107};
src/resinator/source_mapping.zig deleted-687
...@@ -1,687 +0,0 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter;
4const parseQuotedAsciiString = @import("literals.zig").parseQuotedAsciiString;
5const lex = @import("lex.zig");
6
7pub const ParseLineCommandsResult = struct {
8 result: []u8,
9 mappings: SourceMappings,
10};
11
12const CurrentMapping = struct {
13 line_num: usize = 1,
14 filename: std.ArrayListUnmanaged(u8) = .{},
15 pending: bool = true,
16 ignore_contents: bool = false,
17};
18
19pub const ParseAndRemoveLineCommandsOptions = struct {
20 initial_filename: ?[]const u8 = null,
21};
22
23/// Parses and removes #line commands as well as all source code that is within a file
24/// with .c or .h extensions.
25///
26/// > RC treats files with the .c and .h extensions in a special manner. It
27/// > assumes that a file with one of these extensions does not contain
28/// > resources. If a file has the .c or .h file name extension, RC ignores all
29/// > lines in the file except the preprocessor directives. Therefore, to
30/// > include a file that contains resources in another resource script, give
31/// > the file to be included an extension other than .c or .h.
32/// from https://learn.microsoft.com/en-us/windows/win32/menurc/preprocessor-directives
33///
34/// Returns a slice of `buf` with the aforementioned stuff removed as well as a mapping
35/// between the lines and their corresponding lines in their original files.
36///
37/// `buf` must be at least as long as `source`
38/// In-place transformation is supported (i.e. `source` and `buf` can be the same slice)
39///
40/// If `options.initial_filename` is provided, that filename is guaranteed to be
41/// within the `mappings.files` table and `root_filename_offset` will be set appropriately.
42pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult {
43 var parse_result = ParseLineCommandsResult{
44 .result = undefined,
45 .mappings = .{},
46 };
47 errdefer parse_result.mappings.deinit(allocator);
48
49 var current_mapping: CurrentMapping = .{};
50 defer current_mapping.filename.deinit(allocator);
51
52 if (options.initial_filename) |initial_filename| {
53 try current_mapping.filename.appendSlice(allocator, initial_filename);
54 parse_result.mappings.root_filename_offset = try parse_result.mappings.files.put(allocator, initial_filename);
55 }
56
57 std.debug.assert(buf.len >= source.len);
58 var result = UncheckedSliceWriter{ .slice = buf };
59 const State = enum {
60 line_start,
61 preprocessor,
62 non_preprocessor,
63 };
64 var state: State = .line_start;
65 var index: usize = 0;
66 var pending_start: ?usize = null;
67 var preprocessor_start: usize = 0;
68 var line_number: usize = 1;
69 while (index < source.len) : (index += 1) {
70 const c = source[index];
71 switch (state) {
72 .line_start => switch (c) {
73 '#' => {
74 preprocessor_start = index;
75 state = .preprocessor;
76 if (pending_start == null) {
77 pending_start = index;
78 }
79 },
80 '\r', '\n' => {
81 const is_crlf = formsLineEndingPair(source, c, index + 1);
82 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
83 if (!current_mapping.ignore_contents) {
84 result.write(c);
85 if (is_crlf) result.write(source[index + 1]);
86 line_number += 1;
87 }
88 if (is_crlf) index += 1;
89 pending_start = null;
90 },
91 ' ', '\t', '\x0b', '\x0c' => {
92 if (pending_start == null) {
93 pending_start = index;
94 }
95 },
96 else => {
97 state = .non_preprocessor;
98 if (pending_start != null) {
99 if (!current_mapping.ignore_contents) {
100 result.writeSlice(source[pending_start.? .. index + 1]);
101 }
102 pending_start = null;
103 continue;
104 }
105 if (!current_mapping.ignore_contents) {
106 result.write(c);
107 }
108 },
109 },
110 .preprocessor => switch (c) {
111 '\r', '\n' => {
112 // Now that we have the full line we can decide what to do with it
113 const preprocessor_str = source[preprocessor_start..index];
114 const is_crlf = formsLineEndingPair(source, c, index + 1);
115 if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
116 try handleLineCommand(allocator, preprocessor_str, &current_mapping);
117 } else {
118 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
119 if (!current_mapping.ignore_contents) {
120 const line_ending_len: usize = if (is_crlf) 2 else 1;
121 result.writeSlice(source[pending_start.? .. index + line_ending_len]);
122 line_number += 1;
123 }
124 }
125 if (is_crlf) index += 1;
126 state = .line_start;
127 pending_start = null;
128 },
129 else => {},
130 },
131 .non_preprocessor => switch (c) {
132 '\r', '\n' => {
133 const is_crlf = formsLineEndingPair(source, c, index + 1);
134 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
135 if (!current_mapping.ignore_contents) {
136 result.write(c);
137 if (is_crlf) result.write(source[index + 1]);
138 line_number += 1;
139 }
140 if (is_crlf) index += 1;
141 state = .line_start;
142 pending_start = null;
143 },
144 else => {
145 if (!current_mapping.ignore_contents) {
146 result.write(c);
147 }
148 },
149 },
150 }
151 } else {
152 switch (state) {
153 .line_start => {},
154 .non_preprocessor => {
155 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
156 },
157 .preprocessor => {
158 // Now that we have the full line we can decide what to do with it
159 const preprocessor_str = source[preprocessor_start..index];
160 if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
161 try handleLineCommand(allocator, preprocessor_str, &current_mapping);
162 } else {
163 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
164 if (!current_mapping.ignore_contents) {
165 result.writeSlice(source[pending_start.?..index]);
166 }
167 }
168 },
169 }
170 }
171
172 parse_result.result = result.getWritten();
173
174 // Remove whitespace from the end of the result. This avoids issues when the
175 // preprocessor adds a newline to the end of the file, since then the
176 // post-preprocessed source could have more lines than the corresponding input source and
177 // the inserted line can't be mapped to any lines in the original file.
178 // There's no way that whitespace at the end of a file can affect the parsing
179 // of the RC script so this is okay to do unconditionally.
180 // TODO: There might be a better way around this
181 while (parse_result.result.len > 0 and std.ascii.isWhitespace(parse_result.result[parse_result.result.len - 1])) {
182 parse_result.result.len -= 1;
183 }
184
185 // If there have been no line mappings at all, then we're dealing with an empty file.
186 // In this case, we want to fake a line mapping just so that we return something
187 // that is useable in the same way that a non-empty mapping would be.
188 if (parse_result.mappings.mapping.items.len == 0) {
189 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
190 }
191
192 return parse_result;
193}
194
195/// Note: This should function the same as lex.LineHandler.currentIndexFormsLineEndingPair
196pub fn formsLineEndingPair(source: []const u8, line_ending: u8, next_index: usize) bool {
197 if (next_index >= source.len) return false;
198
199 const next_ending = source[next_index];
200 if (next_ending != '\r' and next_ending != '\n') return false;
201
202 // can't be \n\n or \r\r
203 if (line_ending == next_ending) return false;
204
205 return true;
206}
207
208pub fn handleLineEnd(allocator: Allocator, post_processed_line_number: usize, mapping: *SourceMappings, current_mapping: *CurrentMapping) !void {
209 const filename_offset = try mapping.files.put(allocator, current_mapping.filename.items);
210
211 try mapping.set(allocator, post_processed_line_number, .{
212 .start_line = current_mapping.line_num,
213 .end_line = current_mapping.line_num,
214 .filename_offset = filename_offset,
215 });
216
217 current_mapping.line_num += 1;
218 current_mapping.pending = false;
219}
220
221// TODO: Might want to provide diagnostics on invalid line commands instead of just returning
222pub fn handleLineCommand(allocator: Allocator, line_command: []const u8, current_mapping: *CurrentMapping) error{OutOfMemory}!void {
223 // TODO: Are there other whitespace characters that should be included?
224 var tokenizer = std.mem.tokenize(u8, line_command, " \t");
225 const line_directive = tokenizer.next() orelse return; // #line
226 if (!std.mem.eql(u8, line_directive, "#line")) return;
227 const linenum_str = tokenizer.next() orelse return;
228 const linenum = std.fmt.parseUnsigned(usize, linenum_str, 10) catch return;
229
230 var filename_literal = tokenizer.rest();
231 while (filename_literal.len > 0 and std.ascii.isWhitespace(filename_literal[filename_literal.len - 1])) {
232 filename_literal.len -= 1;
233 }
234 if (filename_literal.len < 2) return;
235 const is_quoted = filename_literal[0] == '"' and filename_literal[filename_literal.len - 1] == '"';
236 if (!is_quoted) return;
237 const filename = parseFilename(allocator, filename_literal[1 .. filename_literal.len - 1]) catch |err| switch (err) {
238 error.OutOfMemory => |e| return e,
239 else => return,
240 };
241 defer allocator.free(filename);
242
243 // \x00 bytes in the filename is incompatible with how StringTable works
244 if (std.mem.indexOfScalar(u8, filename, '\x00') != null) return;
245
246 current_mapping.line_num = linenum;
247 current_mapping.filename.clearRetainingCapacity();
248 try current_mapping.filename.appendSlice(allocator, filename);
249 current_mapping.pending = true;
250 current_mapping.ignore_contents = std.ascii.endsWithIgnoreCase(filename, ".c") or std.ascii.endsWithIgnoreCase(filename, ".h");
251}
252
253pub fn parseAndRemoveLineCommandsAlloc(allocator: Allocator, source: []const u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult {
254 const buf = try allocator.alloc(u8, source.len);
255 errdefer allocator.free(buf);
256 var result = try parseAndRemoveLineCommands(allocator, source, buf, options);
257 result.result = try allocator.realloc(buf, result.result.len);
258 return result;
259}
260
261/// C-style string parsing with a few caveats:
262/// - The str cannot contain newlines or carriage returns
263/// - Hex and octal escape are limited to u8
264/// - No handling/support for L, u, or U prefixed strings
265/// - The start and end double quotes should be omitted from the `str`
266/// - Other than the above, does not assume any validity of the strings (i.e. there
267/// may be unescaped double quotes within the str) and will return error.InvalidString
268/// on any problems found.
269///
270/// The result is a UTF-8 encoded string.
271fn parseFilename(allocator: Allocator, str: []const u8) error{ OutOfMemory, InvalidString }![]u8 {
272 const State = enum {
273 string,
274 escape,
275 escape_hex,
276 escape_octal,
277 escape_u,
278 };
279
280 var filename = try std.ArrayList(u8).initCapacity(allocator, str.len);
281 errdefer filename.deinit();
282 var state: State = .string;
283 var index: usize = 0;
284 var escape_len: usize = undefined;
285 var escape_val: u64 = undefined;
286 var escape_expected_len: u8 = undefined;
287 while (index < str.len) : (index += 1) {
288 const c = str[index];
289 switch (state) {
290 .string => switch (c) {
291 '\\' => state = .escape,
292 '"' => return error.InvalidString,
293 else => filename.appendAssumeCapacity(c),
294 },
295 .escape => switch (c) {
296 '\'', '"', '\\', '?', 'n', 'r', 't', 'a', 'b', 'e', 'f', 'v' => {
297 const escaped_c = switch (c) {
298 '\'', '"', '\\', '?' => c,
299 'n' => '\n',
300 'r' => '\r',
301 't' => '\t',
302 'a' => '\x07',
303 'b' => '\x08',
304 'e' => '\x1b', // non-standard
305 'f' => '\x0c',
306 'v' => '\x0b',
307 else => unreachable,
308 };
309 filename.appendAssumeCapacity(escaped_c);
310 state = .string;
311 },
312 'x' => {
313 escape_val = 0;
314 escape_len = 0;
315 state = .escape_hex;
316 },
317 '0'...'7' => {
318 escape_val = std.fmt.charToDigit(c, 8) catch unreachable;
319 escape_len = 1;
320 state = .escape_octal;
321 },
322 'u' => {
323 escape_val = 0;
324 escape_len = 0;
325 state = .escape_u;
326 escape_expected_len = 4;
327 },
328 'U' => {
329 escape_val = 0;
330 escape_len = 0;
331 state = .escape_u;
332 escape_expected_len = 8;
333 },
334 else => return error.InvalidString,
335 },
336 .escape_hex => switch (c) {
337 '0'...'9', 'a'...'f', 'A'...'F' => {
338 const digit = std.fmt.charToDigit(c, 16) catch unreachable;
339 if (escape_val != 0) escape_val = std.math.mul(u8, @as(u8, @intCast(escape_val)), 16) catch return error.InvalidString;
340 escape_val = std.math.add(u8, @as(u8, @intCast(escape_val)), digit) catch return error.InvalidString;
341 escape_len += 1;
342 },
343 else => {
344 if (escape_len == 0) return error.InvalidString;
345 filename.appendAssumeCapacity(@intCast(escape_val));
346 state = .string;
347 index -= 1; // reconsume
348 },
349 },
350 .escape_octal => switch (c) {
351 '0'...'7' => {
352 const digit = std.fmt.charToDigit(c, 8) catch unreachable;
353 if (escape_val != 0) escape_val = std.math.mul(u8, @as(u8, @intCast(escape_val)), 8) catch return error.InvalidString;
354 escape_val = std.math.add(u8, @as(u8, @intCast(escape_val)), digit) catch return error.InvalidString;
355 escape_len += 1;
356 if (escape_len == 3) {
357 filename.appendAssumeCapacity(@intCast(escape_val));
358 state = .string;
359 }
360 },
361 else => {
362 if (escape_len == 0) return error.InvalidString;
363 filename.appendAssumeCapacity(@intCast(escape_val));
364 state = .string;
365 index -= 1; // reconsume
366 },
367 },
368 .escape_u => switch (c) {
369 '0'...'9', 'a'...'f', 'A'...'F' => {
370 const digit = std.fmt.charToDigit(c, 16) catch unreachable;
371 if (escape_val != 0) escape_val = std.math.mul(u21, @as(u21, @intCast(escape_val)), 16) catch return error.InvalidString;
372 escape_val = std.math.add(u21, @as(u21, @intCast(escape_val)), digit) catch return error.InvalidString;
373 escape_len += 1;
374 if (escape_len == escape_expected_len) {
375 var buf: [4]u8 = undefined;
376 const utf8_len = std.unicode.utf8Encode(@intCast(escape_val), &buf) catch return error.InvalidString;
377 filename.appendSliceAssumeCapacity(buf[0..utf8_len]);
378 state = .string;
379 }
380 },
381 // Requires escape_expected_len valid hex digits
382 else => return error.InvalidString,
383 },
384 }
385 } else {
386 switch (state) {
387 .string => {},
388 .escape, .escape_u => return error.InvalidString,
389 .escape_hex => {
390 if (escape_len == 0) return error.InvalidString;
391 filename.appendAssumeCapacity(@intCast(escape_val));
392 },
393 .escape_octal => {
394 filename.appendAssumeCapacity(@intCast(escape_val));
395 },
396 }
397 }
398
399 return filename.toOwnedSlice();
400}
401
402fn testParseFilename(expected: []const u8, input: []const u8) !void {
403 const parsed = try parseFilename(std.testing.allocator, input);
404 defer std.testing.allocator.free(parsed);
405
406 return std.testing.expectEqualSlices(u8, expected, parsed);
407}
408
409test parseFilename {
410 try testParseFilename("'\"?\\\t\n\r\x11", "\\'\\\"\\?\\\\\\t\\n\\r\\x11");
411 try testParseFilename("\xABz\x53", "\\xABz\\123");
412 try testParseFilename("⚡⚡", "\\u26A1\\U000026A1");
413 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\""));
414 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\"));
415 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\u"));
416 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\U"));
417 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\x"));
418 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\xZZ"));
419 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\xABCDEF"));
420 try std.testing.expectError(error.InvalidString, parseFilename(std.testing.allocator, "\\777"));
421}
422
423pub const SourceMappings = struct {
424 /// line number -> span where the index is (line number - 1)
425 mapping: std.ArrayListUnmanaged(SourceSpan) = .{},
426 files: StringTable = .{},
427 /// The default assumes that the first filename added is the root file.
428 /// The value should be set to the correct offset if that assumption does not hold.
429 root_filename_offset: u32 = 0,
430
431 pub const SourceSpan = struct {
432 start_line: usize,
433 end_line: usize,
434 filename_offset: u32,
435 };
436
437 pub fn deinit(self: *SourceMappings, allocator: Allocator) void {
438 self.files.deinit(allocator);
439 self.mapping.deinit(allocator);
440 }
441
442 pub fn set(self: *SourceMappings, allocator: Allocator, line_num: usize, span: SourceSpan) !void {
443 const ptr = try self.expandAndGet(allocator, line_num);
444 ptr.* = span;
445 }
446
447 pub fn has(self: SourceMappings, line_num: usize) bool {
448 return self.mapping.items.len >= line_num;
449 }
450
451 /// Note: `line_num` is 1-indexed
452 pub fn get(self: SourceMappings, line_num: usize) SourceSpan {
453 return self.mapping.items[line_num - 1];
454 }
455
456 pub fn getPtr(self: SourceMappings, line_num: usize) *SourceSpan {
457 return &self.mapping.items[line_num - 1];
458 }
459
460 /// Expands the number of lines in the mapping to include the requested
461 /// line number (if necessary) and returns a pointer to the value at that
462 /// line number.
463 ///
464 /// Note: `line_num` is 1-indexed
465 pub fn expandAndGet(self: *SourceMappings, allocator: Allocator, line_num: usize) !*SourceSpan {
466 try self.mapping.resize(allocator, line_num);
467 return &self.mapping.items[line_num - 1];
468 }
469
470 pub fn collapse(self: *SourceMappings, line_num: usize, num_following_lines_to_collapse: usize) void {
471 std.debug.assert(num_following_lines_to_collapse > 0);
472
473 var span_to_collapse_into = self.getPtr(line_num);
474 const last_collapsed_span = self.get(line_num + num_following_lines_to_collapse);
475 span_to_collapse_into.end_line = last_collapsed_span.end_line;
476
477 const after_collapsed_start = line_num + num_following_lines_to_collapse;
478 const new_num_lines = self.mapping.items.len - num_following_lines_to_collapse;
479 std.mem.copyForwards(SourceSpan, self.mapping.items[line_num..new_num_lines], self.mapping.items[after_collapsed_start..]);
480
481 self.mapping.items.len = new_num_lines;
482 }
483
484 /// Returns true if the line is from the main/root file (i.e. not a file that has been
485 /// `#include`d).
486 pub fn isRootFile(self: *SourceMappings, line_num: usize) bool {
487 const line_mapping = self.get(line_num);
488 if (line_mapping.filename_offset == self.root_filename_offset) return true;
489 return false;
490 }
491};
492
493test "SourceMappings collapse" {
494 const allocator = std.testing.allocator;
495
496 var mappings = SourceMappings{};
497 defer mappings.deinit(allocator);
498 const filename_offset = try mappings.files.put(allocator, "test.rc");
499
500 try mappings.set(allocator, 1, .{ .start_line = 1, .end_line = 1, .filename_offset = filename_offset });
501 try mappings.set(allocator, 2, .{ .start_line = 2, .end_line = 3, .filename_offset = filename_offset });
502 try mappings.set(allocator, 3, .{ .start_line = 4, .end_line = 4, .filename_offset = filename_offset });
503 try mappings.set(allocator, 4, .{ .start_line = 5, .end_line = 5, .filename_offset = filename_offset });
504
505 mappings.collapse(1, 2);
506
507 try std.testing.expectEqual(@as(usize, 2), mappings.mapping.items.len);
508 try std.testing.expectEqual(@as(usize, 4), mappings.mapping.items[0].end_line);
509 try std.testing.expectEqual(@as(usize, 5), mappings.mapping.items[1].end_line);
510}
511
512/// Same thing as StringTable in Zig's src/Wasm.zig
513pub const StringTable = struct {
514 data: std.ArrayListUnmanaged(u8) = .{},
515 map: std.HashMapUnmanaged(u32, void, std.hash_map.StringIndexContext, std.hash_map.default_max_load_percentage) = .{},
516
517 pub fn deinit(self: *StringTable, allocator: Allocator) void {
518 self.data.deinit(allocator);
519 self.map.deinit(allocator);
520 }
521
522 pub fn put(self: *StringTable, allocator: Allocator, value: []const u8) !u32 {
523 const result = try self.map.getOrPutContextAdapted(
524 allocator,
525 value,
526 std.hash_map.StringIndexAdapter{ .bytes = &self.data },
527 .{ .bytes = &self.data },
528 );
529 if (result.found_existing) {
530 return result.key_ptr.*;
531 }
532
533 try self.data.ensureUnusedCapacity(allocator, value.len + 1);
534 const offset: u32 = @intCast(self.data.items.len);
535
536 self.data.appendSliceAssumeCapacity(value);
537 self.data.appendAssumeCapacity(0);
538
539 result.key_ptr.* = offset;
540
541 return offset;
542 }
543
544 pub fn get(self: StringTable, offset: u32) []const u8 {
545 std.debug.assert(offset < self.data.items.len);
546 return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(self.data.items.ptr + offset)), 0);
547 }
548
549 pub fn getOffset(self: *StringTable, value: []const u8) ?u32 {
550 return self.map.getKeyAdapted(
551 value,
552 std.hash_map.StringIndexAdapter{ .bytes = &self.data },
553 );
554 }
555};
556
557const ExpectedSourceSpan = struct {
558 start_line: usize,
559 end_line: usize,
560 filename: []const u8,
561};
562
563fn testParseAndRemoveLineCommands(
564 expected: []const u8,
565 comptime expected_spans: []const ExpectedSourceSpan,
566 source: []const u8,
567 options: ParseAndRemoveLineCommandsOptions,
568) !void {
569 var results = try parseAndRemoveLineCommandsAlloc(std.testing.allocator, source, options);
570 defer std.testing.allocator.free(results.result);
571 defer results.mappings.deinit(std.testing.allocator);
572
573 try std.testing.expectEqualStrings(expected, results.result);
574
575 expectEqualMappings(expected_spans, results.mappings) catch |err| {
576 std.debug.print("\nexpected mappings:\n", .{});
577 for (expected_spans, 0..) |span, i| {
578 const line_num = i + 1;
579 std.debug.print("{}: {s}:{}-{}\n", .{ line_num, span.filename, span.start_line, span.end_line });
580 }
581 std.debug.print("\nactual mappings:\n", .{});
582 for (results.mappings.mapping.items, 0..) |span, i| {
583 const line_num = i + 1;
584 const filename = results.mappings.files.get(span.filename_offset);
585 std.debug.print("{}: {s}:{}-{}\n", .{ line_num, filename, span.start_line, span.end_line });
586 }
587 std.debug.print("\n", .{});
588 return err;
589 };
590}
591
592fn expectEqualMappings(expected_spans: []const ExpectedSourceSpan, mappings: SourceMappings) !void {
593 try std.testing.expectEqual(expected_spans.len, mappings.mapping.items.len);
594 for (expected_spans, 0..) |expected_span, i| {
595 const line_num = i + 1;
596 const span = mappings.get(line_num);
597 const filename = mappings.files.get(span.filename_offset);
598 try std.testing.expectEqual(expected_span.start_line, span.start_line);
599 try std.testing.expectEqual(expected_span.end_line, span.end_line);
600 try std.testing.expectEqualStrings(expected_span.filename, filename);
601 }
602}
603
604test "basic" {
605 try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{
606 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
607 }, "#line 1 \"blah.rc\"", .{});
608}
609
610test "only removes line commands" {
611 try testParseAndRemoveLineCommands(
612 \\#pragma code_page(65001)
613 , &[_]ExpectedSourceSpan{
614 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
615 },
616 \\#line 1 "blah.rc"
617 \\#pragma code_page(65001)
618 , .{});
619}
620
621test "whitespace and line endings" {
622 try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{
623 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
624 }, "#line \t 1 \t \"blah.rc\"\r\n", .{});
625}
626
627test "example" {
628 try testParseAndRemoveLineCommands(
629 \\
630 \\included RCDATA {"hello"}
631 , &[_]ExpectedSourceSpan{
632 .{ .start_line = 1, .end_line = 1, .filename = "./included.rc" },
633 .{ .start_line = 2, .end_line = 2, .filename = "./included.rc" },
634 },
635 \\#line 1 "rcdata.rc"
636 \\#line 1 "<built-in>"
637 \\#line 1 "<built-in>"
638 \\#line 355 "<built-in>"
639 \\#line 1 "<command line>"
640 \\#line 1 "<built-in>"
641 \\#line 1 "rcdata.rc"
642 \\#line 1 "./header.h"
643 \\
644 \\
645 \\2 RCDATA {"blah"}
646 \\
647 \\
648 \\#line 1 "./included.rc"
649 \\
650 \\included RCDATA {"hello"}
651 \\#line 7 "./header.h"
652 \\#line 1 "rcdata.rc"
653 , .{});
654}
655
656test "CRLF and other line endings" {
657 try testParseAndRemoveLineCommands(
658 "hello\r\n#pragma code_page(65001)\r\nworld",
659 &[_]ExpectedSourceSpan{
660 .{ .start_line = 1, .end_line = 1, .filename = "crlf.rc" },
661 .{ .start_line = 2, .end_line = 2, .filename = "crlf.rc" },
662 .{ .start_line = 3, .end_line = 3, .filename = "crlf.rc" },
663 },
664 "#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",
665 .{},
666 );
667}
668
669test "no line commands" {
670 try testParseAndRemoveLineCommands(
671 \\1 RCDATA {"blah"}
672 \\2 RCDATA {"blah"}
673 , &[_]ExpectedSourceSpan{
674 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
675 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
676 },
677 \\1 RCDATA {"blah"}
678 \\2 RCDATA {"blah"}
679 , .{ .initial_filename = "blah.rc" });
680}
681
682test "in place" {
683 var mut_source = "#line 1 \"blah.rc\"".*;
684 var result = try parseAndRemoveLineCommands(std.testing.allocator, &mut_source, &mut_source, .{});
685 defer result.mappings.deinit(std.testing.allocator);
686 try std.testing.expectEqualStrings("", result.result);
687}
src/resinator/utils.zig deleted-112
...@@ -1,112 +0,0 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4/// Like std.io.FixedBufferStream but does no bounds checking
5pub const UncheckedSliceWriter = struct {
6 const Self = @This();
7
8 pos: usize = 0,
9 slice: []u8,
10
11 pub fn write(self: *Self, char: u8) void {
12 self.slice[self.pos] = char;
13 self.pos += 1;
14 }
15
16 pub fn writeSlice(self: *Self, slice: []const u8) void {
17 for (slice) |c| {
18 self.write(c);
19 }
20 }
21
22 pub fn getWritten(self: Self) []u8 {
23 return self.slice[0..self.pos];
24 }
25};
26
27/// Cross-platform 'std.fs.Dir.openFile' wrapper that will always return IsDir if
28/// a directory is attempted to be opened.
29/// TODO: Remove once https://github.com/ziglang/zig/issues/5732 is addressed.
30pub fn openFileNotDir(cwd: std.fs.Dir, path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File {
31 const file = try cwd.openFile(path, flags);
32 errdefer file.close();
33 // https://github.com/ziglang/zig/issues/5732
34 if (builtin.os.tag != .windows) {
35 const stat = try file.stat();
36
37 if (stat.kind == .directory)
38 return error.IsDir;
39 }
40 return file;
41}
42
43/// Emulates the Windows implementation of `iswdigit`, but only returns true
44/// for the non-ASCII digits that `iswdigit` on Windows would return true for.
45pub fn isNonAsciiDigit(c: u21) bool {
46 return switch (c) {
47 '²',
48 '³',
49 '¹',
50 '\u{660}'...'\u{669}',
51 '\u{6F0}'...'\u{6F9}',
52 '\u{7C0}'...'\u{7C9}',
53 '\u{966}'...'\u{96F}',
54 '\u{9E6}'...'\u{9EF}',
55 '\u{A66}'...'\u{A6F}',
56 '\u{AE6}'...'\u{AEF}',
57 '\u{B66}'...'\u{B6F}',
58 '\u{BE6}'...'\u{BEF}',
59 '\u{C66}'...'\u{C6F}',
60 '\u{CE6}'...'\u{CEF}',
61 '\u{D66}'...'\u{D6F}',
62 '\u{E50}'...'\u{E59}',
63 '\u{ED0}'...'\u{ED9}',
64 '\u{F20}'...'\u{F29}',
65 '\u{1040}'...'\u{1049}',
66 '\u{1090}'...'\u{1099}',
67 '\u{17E0}'...'\u{17E9}',
68 '\u{1810}'...'\u{1819}',
69 '\u{1946}'...'\u{194F}',
70 '\u{19D0}'...'\u{19D9}',
71 '\u{1B50}'...'\u{1B59}',
72 '\u{1BB0}'...'\u{1BB9}',
73 '\u{1C40}'...'\u{1C49}',
74 '\u{1C50}'...'\u{1C59}',
75 '\u{A620}'...'\u{A629}',
76 '\u{A8D0}'...'\u{A8D9}',
77 '\u{A900}'...'\u{A909}',
78 '\u{AA50}'...'\u{AA59}',
79 '\u{FF10}'...'\u{FF19}',
80 => true,
81 else => false,
82 };
83}
84
85/// Used for generic colored errors/warnings/notes, more context-specific error messages
86/// are handled elsewhere.
87pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type: enum { err, warning, note }, comptime format: []const u8, args: anytype) !void {
88 switch (msg_type) {
89 .err => {
90 try config.setColor(writer, .bold);
91 try config.setColor(writer, .red);
92 try writer.writeAll("error: ");
93 },
94 .warning => {
95 try config.setColor(writer, .bold);
96 try config.setColor(writer, .yellow);
97 try writer.writeAll("warning: ");
98 },
99 .note => {
100 try config.setColor(writer, .reset);
101 try config.setColor(writer, .cyan);
102 try writer.writeAll("note: ");
103 },
104 }
105 try config.setColor(writer, .reset);
106 if (msg_type == .err) {
107 try config.setColor(writer, .bold);
108 }
109 try writer.print(format, args);
110 try writer.writeByte('\n');
111 try config.setColor(writer, .reset);
112}
src/resinator/windows1252.zig deleted-588
...@@ -1,588 +0,0 @@
1const std = @import("std");
2
3pub fn windows1252ToUtf8Stream(writer: anytype, reader: anytype) !usize {
4 var bytes_written: usize = 0;
5 var utf8_buf: [3]u8 = undefined;
6 while (true) {
7 const c = reader.readByte() catch |err| switch (err) {
8 error.EndOfStream => return bytes_written,
9 else => |e| return e,
10 };
11 const codepoint = toCodepoint(c);
12 if (codepoint <= 0x7F) {
13 try writer.writeByte(c);
14 bytes_written += 1;
15 } else {
16 const utf8_len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch unreachable;
17 try writer.writeAll(utf8_buf[0..utf8_len]);
18 bytes_written += utf8_len;
19 }
20 }
21}
22
23/// Returns the number of code units written to the writer
24pub fn windows1252ToUtf16AllocZ(allocator: std.mem.Allocator, win1252_str: []const u8) ![:0]u16 {
25 // Guaranteed to need exactly the same number of code units as Windows-1252 bytes
26 var utf16_slice = try allocator.allocSentinel(u16, win1252_str.len, 0);
27 errdefer allocator.free(utf16_slice);
28 for (win1252_str, 0..) |c, i| {
29 utf16_slice[i] = toCodepoint(c);
30 }
31 return utf16_slice;
32}
33
34/// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt
35pub fn toCodepoint(c: u8) u16 {
36 return switch (c) {
37 0x80 => 0x20ac, // Euro Sign
38 0x82 => 0x201a, // Single Low-9 Quotation Mark
39 0x83 => 0x0192, // Latin Small Letter F With Hook
40 0x84 => 0x201e, // Double Low-9 Quotation Mark
41 0x85 => 0x2026, // Horizontal Ellipsis
42 0x86 => 0x2020, // Dagger
43 0x87 => 0x2021, // Double Dagger
44 0x88 => 0x02c6, // Modifier Letter Circumflex Accent
45 0x89 => 0x2030, // Per Mille Sign
46 0x8a => 0x0160, // Latin Capital Letter S With Caron
47 0x8b => 0x2039, // Single Left-Pointing Angle Quotation Mark
48 0x8c => 0x0152, // Latin Capital Ligature Oe
49 0x8e => 0x017d, // Latin Capital Letter Z With Caron
50 0x91 => 0x2018, // Left Single Quotation Mark
51 0x92 => 0x2019, // Right Single Quotation Mark
52 0x93 => 0x201c, // Left Double Quotation Mark
53 0x94 => 0x201d, // Right Double Quotation Mark
54 0x95 => 0x2022, // Bullet
55 0x96 => 0x2013, // En Dash
56 0x97 => 0x2014, // Em Dash
57 0x98 => 0x02dc, // Small Tilde
58 0x99 => 0x2122, // Trade Mark Sign
59 0x9a => 0x0161, // Latin Small Letter S With Caron
60 0x9b => 0x203a, // Single Right-Pointing Angle Quotation Mark
61 0x9c => 0x0153, // Latin Small Ligature Oe
62 0x9e => 0x017e, // Latin Small Letter Z With Caron
63 0x9f => 0x0178, // Latin Capital Letter Y With Diaeresis
64 else => c,
65 };
66}
67
68/// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt
69/// Plus some mappings found empirically by iterating all codepoints:
70/// 0x2007 => 0xA0, // Figure Space
71/// 0x2008 => ' ', // Punctuation Space
72/// 0x2009 => ' ', // Thin Space
73/// 0x200A => ' ', // Hair Space
74/// 0x2012 => '-', // Figure Dash
75/// 0x2015 => '-', // Horizontal Bar
76/// 0x201B => '\'', // Single High-reversed-9 Quotation Mark
77/// 0x201F => '"', // Double High-reversed-9 Quotation Mark
78/// 0x202F => 0xA0, // Narrow No-Break Space
79/// 0x2033 => '"', // Double Prime
80/// 0x2036 => '"', // Reversed Double Prime
81pub fn bestFitFromCodepoint(codepoint: u21) ?u8 {
82 return switch (codepoint) {
83 0x00...0x7F,
84 0x81,
85 0x8D,
86 0x8F,
87 0x90,
88 0x9D,
89 0xA0...0xFF,
90 => @intCast(codepoint),
91 0x0100 => 0x41, // Latin Capital Letter A With Macron
92 0x0101 => 0x61, // Latin Small Letter A With Macron
93 0x0102 => 0x41, // Latin Capital Letter A With Breve
94 0x0103 => 0x61, // Latin Small Letter A With Breve
95 0x0104 => 0x41, // Latin Capital Letter A With Ogonek
96 0x0105 => 0x61, // Latin Small Letter A With Ogonek
97 0x0106 => 0x43, // Latin Capital Letter C With Acute
98 0x0107 => 0x63, // Latin Small Letter C With Acute
99 0x0108 => 0x43, // Latin Capital Letter C With Circumflex
100 0x0109 => 0x63, // Latin Small Letter C With Circumflex
101 0x010a => 0x43, // Latin Capital Letter C With Dot Above
102 0x010b => 0x63, // Latin Small Letter C With Dot Above
103 0x010c => 0x43, // Latin Capital Letter C With Caron
104 0x010d => 0x63, // Latin Small Letter C With Caron
105 0x010e => 0x44, // Latin Capital Letter D With Caron
106 0x010f => 0x64, // Latin Small Letter D With Caron
107 0x0110 => 0xd0, // Latin Capital Letter D With Stroke
108 0x0111 => 0x64, // Latin Small Letter D With Stroke
109 0x0112 => 0x45, // Latin Capital Letter E With Macron
110 0x0113 => 0x65, // Latin Small Letter E With Macron
111 0x0114 => 0x45, // Latin Capital Letter E With Breve
112 0x0115 => 0x65, // Latin Small Letter E With Breve
113 0x0116 => 0x45, // Latin Capital Letter E With Dot Above
114 0x0117 => 0x65, // Latin Small Letter E With Dot Above
115 0x0118 => 0x45, // Latin Capital Letter E With Ogonek
116 0x0119 => 0x65, // Latin Small Letter E With Ogonek
117 0x011a => 0x45, // Latin Capital Letter E With Caron
118 0x011b => 0x65, // Latin Small Letter E With Caron
119 0x011c => 0x47, // Latin Capital Letter G With Circumflex
120 0x011d => 0x67, // Latin Small Letter G With Circumflex
121 0x011e => 0x47, // Latin Capital Letter G With Breve
122 0x011f => 0x67, // Latin Small Letter G With Breve
123 0x0120 => 0x47, // Latin Capital Letter G With Dot Above
124 0x0121 => 0x67, // Latin Small Letter G With Dot Above
125 0x0122 => 0x47, // Latin Capital Letter G With Cedilla
126 0x0123 => 0x67, // Latin Small Letter G With Cedilla
127 0x0124 => 0x48, // Latin Capital Letter H With Circumflex
128 0x0125 => 0x68, // Latin Small Letter H With Circumflex
129 0x0126 => 0x48, // Latin Capital Letter H With Stroke
130 0x0127 => 0x68, // Latin Small Letter H With Stroke
131 0x0128 => 0x49, // Latin Capital Letter I With Tilde
132 0x0129 => 0x69, // Latin Small Letter I With Tilde
133 0x012a => 0x49, // Latin Capital Letter I With Macron
134 0x012b => 0x69, // Latin Small Letter I With Macron
135 0x012c => 0x49, // Latin Capital Letter I With Breve
136 0x012d => 0x69, // Latin Small Letter I With Breve
137 0x012e => 0x49, // Latin Capital Letter I With Ogonek
138 0x012f => 0x69, // Latin Small Letter I With Ogonek
139 0x0130 => 0x49, // Latin Capital Letter I With Dot Above
140 0x0131 => 0x69, // Latin Small Letter Dotless I
141 0x0134 => 0x4a, // Latin Capital Letter J With Circumflex
142 0x0135 => 0x6a, // Latin Small Letter J With Circumflex
143 0x0136 => 0x4b, // Latin Capital Letter K With Cedilla
144 0x0137 => 0x6b, // Latin Small Letter K With Cedilla
145 0x0139 => 0x4c, // Latin Capital Letter L With Acute
146 0x013a => 0x6c, // Latin Small Letter L With Acute
147 0x013b => 0x4c, // Latin Capital Letter L With Cedilla
148 0x013c => 0x6c, // Latin Small Letter L With Cedilla
149 0x013d => 0x4c, // Latin Capital Letter L With Caron
150 0x013e => 0x6c, // Latin Small Letter L With Caron
151 0x0141 => 0x4c, // Latin Capital Letter L With Stroke
152 0x0142 => 0x6c, // Latin Small Letter L With Stroke
153 0x0143 => 0x4e, // Latin Capital Letter N With Acute
154 0x0144 => 0x6e, // Latin Small Letter N With Acute
155 0x0145 => 0x4e, // Latin Capital Letter N With Cedilla
156 0x0146 => 0x6e, // Latin Small Letter N With Cedilla
157 0x0147 => 0x4e, // Latin Capital Letter N With Caron
158 0x0148 => 0x6e, // Latin Small Letter N With Caron
159 0x014c => 0x4f, // Latin Capital Letter O With Macron
160 0x014d => 0x6f, // Latin Small Letter O With Macron
161 0x014e => 0x4f, // Latin Capital Letter O With Breve
162 0x014f => 0x6f, // Latin Small Letter O With Breve
163 0x0150 => 0x4f, // Latin Capital Letter O With Double Acute
164 0x0151 => 0x6f, // Latin Small Letter O With Double Acute
165 0x0152 => 0x8c, // Latin Capital Ligature Oe
166 0x0153 => 0x9c, // Latin Small Ligature Oe
167 0x0154 => 0x52, // Latin Capital Letter R With Acute
168 0x0155 => 0x72, // Latin Small Letter R With Acute
169 0x0156 => 0x52, // Latin Capital Letter R With Cedilla
170 0x0157 => 0x72, // Latin Small Letter R With Cedilla
171 0x0158 => 0x52, // Latin Capital Letter R With Caron
172 0x0159 => 0x72, // Latin Small Letter R With Caron
173 0x015a => 0x53, // Latin Capital Letter S With Acute
174 0x015b => 0x73, // Latin Small Letter S With Acute
175 0x015c => 0x53, // Latin Capital Letter S With Circumflex
176 0x015d => 0x73, // Latin Small Letter S With Circumflex
177 0x015e => 0x53, // Latin Capital Letter S With Cedilla
178 0x015f => 0x73, // Latin Small Letter S With Cedilla
179 0x0160 => 0x8a, // Latin Capital Letter S With Caron
180 0x0161 => 0x9a, // Latin Small Letter S With Caron
181 0x0162 => 0x54, // Latin Capital Letter T With Cedilla
182 0x0163 => 0x74, // Latin Small Letter T With Cedilla
183 0x0164 => 0x54, // Latin Capital Letter T With Caron
184 0x0165 => 0x74, // Latin Small Letter T With Caron
185 0x0166 => 0x54, // Latin Capital Letter T With Stroke
186 0x0167 => 0x74, // Latin Small Letter T With Stroke
187 0x0168 => 0x55, // Latin Capital Letter U With Tilde
188 0x0169 => 0x75, // Latin Small Letter U With Tilde
189 0x016a => 0x55, // Latin Capital Letter U With Macron
190 0x016b => 0x75, // Latin Small Letter U With Macron
191 0x016c => 0x55, // Latin Capital Letter U With Breve
192 0x016d => 0x75, // Latin Small Letter U With Breve
193 0x016e => 0x55, // Latin Capital Letter U With Ring Above
194 0x016f => 0x75, // Latin Small Letter U With Ring Above
195 0x0170 => 0x55, // Latin Capital Letter U With Double Acute
196 0x0171 => 0x75, // Latin Small Letter U With Double Acute
197 0x0172 => 0x55, // Latin Capital Letter U With Ogonek
198 0x0173 => 0x75, // Latin Small Letter U With Ogonek
199 0x0174 => 0x57, // Latin Capital Letter W With Circumflex
200 0x0175 => 0x77, // Latin Small Letter W With Circumflex
201 0x0176 => 0x59, // Latin Capital Letter Y With Circumflex
202 0x0177 => 0x79, // Latin Small Letter Y With Circumflex
203 0x0178 => 0x9f, // Latin Capital Letter Y With Diaeresis
204 0x0179 => 0x5a, // Latin Capital Letter Z With Acute
205 0x017a => 0x7a, // Latin Small Letter Z With Acute
206 0x017b => 0x5a, // Latin Capital Letter Z With Dot Above
207 0x017c => 0x7a, // Latin Small Letter Z With Dot Above
208 0x017d => 0x8e, // Latin Capital Letter Z With Caron
209 0x017e => 0x9e, // Latin Small Letter Z With Caron
210 0x0180 => 0x62, // Latin Small Letter B With Stroke
211 0x0189 => 0xd0, // Latin Capital Letter African D
212 0x0191 => 0x83, // Latin Capital Letter F With Hook
213 0x0192 => 0x83, // Latin Small Letter F With Hook
214 0x0197 => 0x49, // Latin Capital Letter I With Stroke
215 0x019a => 0x6c, // Latin Small Letter L With Bar
216 0x019f => 0x4f, // Latin Capital Letter O With Middle Tilde
217 0x01a0 => 0x4f, // Latin Capital Letter O With Horn
218 0x01a1 => 0x6f, // Latin Small Letter O With Horn
219 0x01ab => 0x74, // Latin Small Letter T With Palatal Hook
220 0x01ae => 0x54, // Latin Capital Letter T With Retroflex Hook
221 0x01af => 0x55, // Latin Capital Letter U With Horn
222 0x01b0 => 0x75, // Latin Small Letter U With Horn
223 0x01b6 => 0x7a, // Latin Small Letter Z With Stroke
224 0x01c0 => 0x7c, // Latin Letter Dental Click
225 0x01c3 => 0x21, // Latin Letter Retroflex Click
226 0x01cd => 0x41, // Latin Capital Letter A With Caron
227 0x01ce => 0x61, // Latin Small Letter A With Caron
228 0x01cf => 0x49, // Latin Capital Letter I With Caron
229 0x01d0 => 0x69, // Latin Small Letter I With Caron
230 0x01d1 => 0x4f, // Latin Capital Letter O With Caron
231 0x01d2 => 0x6f, // Latin Small Letter O With Caron
232 0x01d3 => 0x55, // Latin Capital Letter U With Caron
233 0x01d4 => 0x75, // Latin Small Letter U With Caron
234 0x01d5 => 0x55, // Latin Capital Letter U With Diaeresis And Macron
235 0x01d6 => 0x75, // Latin Small Letter U With Diaeresis And Macron
236 0x01d7 => 0x55, // Latin Capital Letter U With Diaeresis And Acute
237 0x01d8 => 0x75, // Latin Small Letter U With Diaeresis And Acute
238 0x01d9 => 0x55, // Latin Capital Letter U With Diaeresis And Caron
239 0x01da => 0x75, // Latin Small Letter U With Diaeresis And Caron
240 0x01db => 0x55, // Latin Capital Letter U With Diaeresis And Grave
241 0x01dc => 0x75, // Latin Small Letter U With Diaeresis And Grave
242 0x01de => 0x41, // Latin Capital Letter A With Diaeresis And Macron
243 0x01df => 0x61, // Latin Small Letter A With Diaeresis And Macron
244 0x01e4 => 0x47, // Latin Capital Letter G With Stroke
245 0x01e5 => 0x67, // Latin Small Letter G With Stroke
246 0x01e6 => 0x47, // Latin Capital Letter G With Caron
247 0x01e7 => 0x67, // Latin Small Letter G With Caron
248 0x01e8 => 0x4b, // Latin Capital Letter K With Caron
249 0x01e9 => 0x6b, // Latin Small Letter K With Caron
250 0x01ea => 0x4f, // Latin Capital Letter O With Ogonek
251 0x01eb => 0x6f, // Latin Small Letter O With Ogonek
252 0x01ec => 0x4f, // Latin Capital Letter O With Ogonek And Macron
253 0x01ed => 0x6f, // Latin Small Letter O With Ogonek And Macron
254 0x01f0 => 0x6a, // Latin Small Letter J With Caron
255 0x0261 => 0x67, // Latin Small Letter Script G
256 0x02b9 => 0x27, // Modifier Letter Prime
257 0x02ba => 0x22, // Modifier Letter Double Prime
258 0x02bc => 0x27, // Modifier Letter Apostrophe
259 0x02c4 => 0x5e, // Modifier Letter Up Arrowhead
260 0x02c6 => 0x88, // Modifier Letter Circumflex Accent
261 0x02c8 => 0x27, // Modifier Letter Vertical Line
262 0x02c9 => 0xaf, // Modifier Letter Macron
263 0x02ca => 0xb4, // Modifier Letter Acute Accent
264 0x02cb => 0x60, // Modifier Letter Grave Accent
265 0x02cd => 0x5f, // Modifier Letter Low Macron
266 0x02da => 0xb0, // Ring Above
267 0x02dc => 0x98, // Small Tilde
268 0x0300 => 0x60, // Combining Grave Accent
269 0x0301 => 0xb4, // Combining Acute Accent
270 0x0302 => 0x5e, // Combining Circumflex Accent
271 0x0303 => 0x7e, // Combining Tilde
272 0x0304 => 0xaf, // Combining Macron
273 0x0305 => 0xaf, // Combining Overline
274 0x0308 => 0xa8, // Combining Diaeresis
275 0x030a => 0xb0, // Combining Ring Above
276 0x030e => 0x22, // Combining Double Vertical Line Above
277 0x0327 => 0xb8, // Combining Cedilla
278 0x0331 => 0x5f, // Combining Macron Below
279 0x0332 => 0x5f, // Combining Low Line
280 0x037e => 0x3b, // Greek Question Mark
281 0x0393 => 0x47, // Greek Capital Letter Gamma
282 0x0398 => 0x54, // Greek Capital Letter Theta
283 0x03a3 => 0x53, // Greek Capital Letter Sigma
284 0x03a6 => 0x46, // Greek Capital Letter Phi
285 0x03a9 => 0x4f, // Greek Capital Letter Omega
286 0x03b1 => 0x61, // Greek Small Letter Alpha
287 0x03b2 => 0xdf, // Greek Small Letter Beta
288 0x03b4 => 0x64, // Greek Small Letter Delta
289 0x03b5 => 0x65, // Greek Small Letter Epsilon
290 0x03bc => 0xb5, // Greek Small Letter Mu
291 0x03c0 => 0x70, // Greek Small Letter Pi
292 0x03c3 => 0x73, // Greek Small Letter Sigma
293 0x03c4 => 0x74, // Greek Small Letter Tau
294 0x03c6 => 0x66, // Greek Small Letter Phi
295 0x04bb => 0x68, // Cyrillic Small Letter Shha
296 0x0589 => 0x3a, // Armenian Full Stop
297 0x066a => 0x25, // Arabic Percent Sign
298 0x2000 => 0x20, // En Quad
299 0x2001 => 0x20, // Em Quad
300 0x2002 => 0x20, // En Space
301 0x2003 => 0x20, // Em Space
302 0x2004 => 0x20, // Three-Per-Em Space
303 0x2005 => 0x20, // Four-Per-Em Space
304 0x2006 => 0x20, // Six-Per-Em Space
305 0x2010 => 0x2d, // Hyphen
306 0x2011 => 0x2d, // Non-Breaking Hyphen
307 0x2013 => 0x96, // En Dash
308 0x2014 => 0x97, // Em Dash
309 0x2017 => 0x3d, // Double Low Line
310 0x2018 => 0x91, // Left Single Quotation Mark
311 0x2019 => 0x92, // Right Single Quotation Mark
312 0x201a => 0x82, // Single Low-9 Quotation Mark
313 0x201c => 0x93, // Left Double Quotation Mark
314 0x201d => 0x94, // Right Double Quotation Mark
315 0x201e => 0x84, // Double Low-9 Quotation Mark
316 0x2020 => 0x86, // Dagger
317 0x2021 => 0x87, // Double Dagger
318 0x2022 => 0x95, // Bullet
319 0x2024 => 0xb7, // One Dot Leader
320 0x2026 => 0x85, // Horizontal Ellipsis
321 0x2030 => 0x89, // Per Mille Sign
322 0x2032 => 0x27, // Prime
323 0x2035 => 0x60, // Reversed Prime
324 0x2039 => 0x8b, // Single Left-Pointing Angle Quotation Mark
325 0x203a => 0x9b, // Single Right-Pointing Angle Quotation Mark
326 0x2044 => 0x2f, // Fraction Slash
327 0x2070 => 0xb0, // Superscript Zero
328 0x2074 => 0x34, // Superscript Four
329 0x2075 => 0x35, // Superscript Five
330 0x2076 => 0x36, // Superscript Six
331 0x2077 => 0x37, // Superscript Seven
332 0x2078 => 0x38, // Superscript Eight
333 0x207f => 0x6e, // Superscript Latin Small Letter N
334 0x2080 => 0x30, // Subscript Zero
335 0x2081 => 0x31, // Subscript One
336 0x2082 => 0x32, // Subscript Two
337 0x2083 => 0x33, // Subscript Three
338 0x2084 => 0x34, // Subscript Four
339 0x2085 => 0x35, // Subscript Five
340 0x2086 => 0x36, // Subscript Six
341 0x2087 => 0x37, // Subscript Seven
342 0x2088 => 0x38, // Subscript Eight
343 0x2089 => 0x39, // Subscript Nine
344 0x20ac => 0x80, // Euro Sign
345 0x20a1 => 0xa2, // Colon Sign
346 0x20a4 => 0xa3, // Lira Sign
347 0x20a7 => 0x50, // Peseta Sign
348 0x2102 => 0x43, // Double-Struck Capital C
349 0x2107 => 0x45, // Euler Constant
350 0x210a => 0x67, // Script Small G
351 0x210b => 0x48, // Script Capital H
352 0x210c => 0x48, // Black-Letter Capital H
353 0x210d => 0x48, // Double-Struck Capital H
354 0x210e => 0x68, // Planck Constant
355 0x2110 => 0x49, // Script Capital I
356 0x2111 => 0x49, // Black-Letter Capital I
357 0x2112 => 0x4c, // Script Capital L
358 0x2113 => 0x6c, // Script Small L
359 0x2115 => 0x4e, // Double-Struck Capital N
360 0x2118 => 0x50, // Script Capital P
361 0x2119 => 0x50, // Double-Struck Capital P
362 0x211a => 0x51, // Double-Struck Capital Q
363 0x211b => 0x52, // Script Capital R
364 0x211c => 0x52, // Black-Letter Capital R
365 0x211d => 0x52, // Double-Struck Capital R
366 0x2122 => 0x99, // Trade Mark Sign
367 0x2124 => 0x5a, // Double-Struck Capital Z
368 0x2128 => 0x5a, // Black-Letter Capital Z
369 0x212a => 0x4b, // Kelvin Sign
370 0x212b => 0xc5, // Angstrom Sign
371 0x212c => 0x42, // Script Capital B
372 0x212d => 0x43, // Black-Letter Capital C
373 0x212e => 0x65, // Estimated Symbol
374 0x212f => 0x65, // Script Small E
375 0x2130 => 0x45, // Script Capital E
376 0x2131 => 0x46, // Script Capital F
377 0x2133 => 0x4d, // Script Capital M
378 0x2134 => 0x6f, // Script Small O
379 0x2205 => 0xd8, // Empty Set
380 0x2212 => 0x2d, // Minus Sign
381 0x2213 => 0xb1, // Minus-Or-Plus Sign
382 0x2215 => 0x2f, // Division Slash
383 0x2216 => 0x5c, // Set Minus
384 0x2217 => 0x2a, // Asterisk Operator
385 0x2218 => 0xb0, // Ring Operator
386 0x2219 => 0xb7, // Bullet Operator
387 0x221a => 0x76, // Square Root
388 0x221e => 0x38, // Infinity
389 0x2223 => 0x7c, // Divides
390 0x2229 => 0x6e, // Intersection
391 0x2236 => 0x3a, // Ratio
392 0x223c => 0x7e, // Tilde Operator
393 0x2248 => 0x98, // Almost Equal To
394 0x2261 => 0x3d, // Identical To
395 0x2264 => 0x3d, // Less-Than Or Equal To
396 0x2265 => 0x3d, // Greater-Than Or Equal To
397 0x226a => 0xab, // Much Less-Than
398 0x226b => 0xbb, // Much Greater-Than
399 0x22c5 => 0xb7, // Dot Operator
400 0x2302 => 0xa6, // House
401 0x2303 => 0x5e, // Up Arrowhead
402 0x2310 => 0xac, // Reversed Not Sign
403 0x2320 => 0x28, // Top Half Integral
404 0x2321 => 0x29, // Bottom Half Integral
405 0x2329 => 0x3c, // Left-Pointing Angle Bracket
406 0x232a => 0x3e, // Right-Pointing Angle Bracket
407 0x2500 => 0x2d, // Box Drawings Light Horizontal
408 0x2502 => 0xa6, // Box Drawings Light Vertical
409 0x250c => 0x2b, // Box Drawings Light Down And Right
410 0x2510 => 0x2b, // Box Drawings Light Down And Left
411 0x2514 => 0x2b, // Box Drawings Light Up And Right
412 0x2518 => 0x2b, // Box Drawings Light Up And Left
413 0x251c => 0x2b, // Box Drawings Light Vertical And Right
414 0x2524 => 0xa6, // Box Drawings Light Vertical And Left
415 0x252c => 0x2d, // Box Drawings Light Down And Horizontal
416 0x2534 => 0x2d, // Box Drawings Light Up And Horizontal
417 0x253c => 0x2b, // Box Drawings Light Vertical And Horizontal
418 0x2550 => 0x2d, // Box Drawings Double Horizontal
419 0x2551 => 0xa6, // Box Drawings Double Vertical
420 0x2552 => 0x2b, // Box Drawings Down Single And Right Double
421 0x2553 => 0x2b, // Box Drawings Down Double And Right Single
422 0x2554 => 0x2b, // Box Drawings Double Down And Right
423 0x2555 => 0x2b, // Box Drawings Down Single And Left Double
424 0x2556 => 0x2b, // Box Drawings Down Double And Left Single
425 0x2557 => 0x2b, // Box Drawings Double Down And Left
426 0x2558 => 0x2b, // Box Drawings Up Single And Right Double
427 0x2559 => 0x2b, // Box Drawings Up Double And Right Single
428 0x255a => 0x2b, // Box Drawings Double Up And Right
429 0x255b => 0x2b, // Box Drawings Up Single And Left Double
430 0x255c => 0x2b, // Box Drawings Up Double And Left Single
431 0x255d => 0x2b, // Box Drawings Double Up And Left
432 0x255e => 0xa6, // Box Drawings Vertical Single And Right Double
433 0x255f => 0xa6, // Box Drawings Vertical Double And Right Single
434 0x2560 => 0xa6, // Box Drawings Double Vertical And Right
435 0x2561 => 0xa6, // Box Drawings Vertical Single And Left Double
436 0x2562 => 0xa6, // Box Drawings Vertical Double And Left Single
437 0x2563 => 0xa6, // Box Drawings Double Vertical And Left
438 0x2564 => 0x2d, // Box Drawings Down Single And Horizontal Double
439 0x2565 => 0x2d, // Box Drawings Down Double And Horizontal Single
440 0x2566 => 0x2d, // Box Drawings Double Down And Horizontal
441 0x2567 => 0x2d, // Box Drawings Up Single And Horizontal Double
442 0x2568 => 0x2d, // Box Drawings Up Double And Horizontal Single
443 0x2569 => 0x2d, // Box Drawings Double Up And Horizontal
444 0x256a => 0x2b, // Box Drawings Vertical Single And Horizontal Double
445 0x256b => 0x2b, // Box Drawings Vertical Double And Horizontal Single
446 0x256c => 0x2b, // Box Drawings Double Vertical And Horizontal
447 0x2580 => 0xaf, // Upper Half Block
448 0x2584 => 0x5f, // Lower Half Block
449 0x2588 => 0xa6, // Full Block
450 0x258c => 0xa6, // Left Half Block
451 0x2590 => 0xa6, // Right Half Block
452 0x2591 => 0xa6, // Light Shade
453 0x2592 => 0xa6, // Medium Shade
454 0x2593 => 0xa6, // Dark Shade
455 0x25a0 => 0xa6, // Black Square
456 0x263c => 0xa4, // White Sun With Rays
457 0x2758 => 0x7c, // Light Vertical Bar
458 0x3000 => 0x20, // Ideographic Space
459 0x3008 => 0x3c, // Left Angle Bracket
460 0x3009 => 0x3e, // Right Angle Bracket
461 0x300a => 0xab, // Left Double Angle Bracket
462 0x300b => 0xbb, // Right Double Angle Bracket
463 0x301a => 0x5b, // Left White Square Bracket
464 0x301b => 0x5d, // Right White Square Bracket
465 0x30fb => 0xb7, // Katakana Middle Dot
466 0xff01 => 0x21, // Fullwidth Exclamation Mark
467 0xff02 => 0x22, // Fullwidth Quotation Mark
468 0xff03 => 0x23, // Fullwidth Number Sign
469 0xff04 => 0x24, // Fullwidth Dollar Sign
470 0xff05 => 0x25, // Fullwidth Percent Sign
471 0xff06 => 0x26, // Fullwidth Ampersand
472 0xff07 => 0x27, // Fullwidth Apostrophe
473 0xff08 => 0x28, // Fullwidth Left Parenthesis
474 0xff09 => 0x29, // Fullwidth Right Parenthesis
475 0xff0a => 0x2a, // Fullwidth Asterisk
476 0xff0b => 0x2b, // Fullwidth Plus Sign
477 0xff0c => 0x2c, // Fullwidth Comma
478 0xff0d => 0x2d, // Fullwidth Hyphen-Minus
479 0xff0e => 0x2e, // Fullwidth Full Stop
480 0xff0f => 0x2f, // Fullwidth Solidus
481 0xff10 => 0x30, // Fullwidth Digit Zero
482 0xff11 => 0x31, // Fullwidth Digit One
483 0xff12 => 0x32, // Fullwidth Digit Two
484 0xff13 => 0x33, // Fullwidth Digit Three
485 0xff14 => 0x34, // Fullwidth Digit Four
486 0xff15 => 0x35, // Fullwidth Digit Five
487 0xff16 => 0x36, // Fullwidth Digit Six
488 0xff17 => 0x37, // Fullwidth Digit Seven
489 0xff18 => 0x38, // Fullwidth Digit Eight
490 0xff19 => 0x39, // Fullwidth Digit Nine
491 0xff1a => 0x3a, // Fullwidth Colon
492 0xff1b => 0x3b, // Fullwidth Semicolon
493 0xff1c => 0x3c, // Fullwidth Less-Than Sign
494 0xff1d => 0x3d, // Fullwidth Equals Sign
495 0xff1e => 0x3e, // Fullwidth Greater-Than Sign
496 0xff1f => 0x3f, // Fullwidth Question Mark
497 0xff20 => 0x40, // Fullwidth Commercial At
498 0xff21 => 0x41, // Fullwidth Latin Capital Letter A
499 0xff22 => 0x42, // Fullwidth Latin Capital Letter B
500 0xff23 => 0x43, // Fullwidth Latin Capital Letter C
501 0xff24 => 0x44, // Fullwidth Latin Capital Letter D
502 0xff25 => 0x45, // Fullwidth Latin Capital Letter E
503 0xff26 => 0x46, // Fullwidth Latin Capital Letter F
504 0xff27 => 0x47, // Fullwidth Latin Capital Letter G
505 0xff28 => 0x48, // Fullwidth Latin Capital Letter H
506 0xff29 => 0x49, // Fullwidth Latin Capital Letter I
507 0xff2a => 0x4a, // Fullwidth Latin Capital Letter J
508 0xff2b => 0x4b, // Fullwidth Latin Capital Letter K
509 0xff2c => 0x4c, // Fullwidth Latin Capital Letter L
510 0xff2d => 0x4d, // Fullwidth Latin Capital Letter M
511 0xff2e => 0x4e, // Fullwidth Latin Capital Letter N
512 0xff2f => 0x4f, // Fullwidth Latin Capital Letter O
513 0xff30 => 0x50, // Fullwidth Latin Capital Letter P
514 0xff31 => 0x51, // Fullwidth Latin Capital Letter Q
515 0xff32 => 0x52, // Fullwidth Latin Capital Letter R
516 0xff33 => 0x53, // Fullwidth Latin Capital Letter S
517 0xff34 => 0x54, // Fullwidth Latin Capital Letter T
518 0xff35 => 0x55, // Fullwidth Latin Capital Letter U
519 0xff36 => 0x56, // Fullwidth Latin Capital Letter V
520 0xff37 => 0x57, // Fullwidth Latin Capital Letter W
521 0xff38 => 0x58, // Fullwidth Latin Capital Letter X
522 0xff39 => 0x59, // Fullwidth Latin Capital Letter Y
523 0xff3a => 0x5a, // Fullwidth Latin Capital Letter Z
524 0xff3b => 0x5b, // Fullwidth Left Square Bracket
525 0xff3c => 0x5c, // Fullwidth Reverse Solidus
526 0xff3d => 0x5d, // Fullwidth Right Square Bracket
527 0xff3e => 0x5e, // Fullwidth Circumflex Accent
528 0xff3f => 0x5f, // Fullwidth Low Line
529 0xff40 => 0x60, // Fullwidth Grave Accent
530 0xff41 => 0x61, // Fullwidth Latin Small Letter A
531 0xff42 => 0x62, // Fullwidth Latin Small Letter B
532 0xff43 => 0x63, // Fullwidth Latin Small Letter C
533 0xff44 => 0x64, // Fullwidth Latin Small Letter D
534 0xff45 => 0x65, // Fullwidth Latin Small Letter E
535 0xff46 => 0x66, // Fullwidth Latin Small Letter F
536 0xff47 => 0x67, // Fullwidth Latin Small Letter G
537 0xff48 => 0x68, // Fullwidth Latin Small Letter H
538 0xff49 => 0x69, // Fullwidth Latin Small Letter I
539 0xff4a => 0x6a, // Fullwidth Latin Small Letter J
540 0xff4b => 0x6b, // Fullwidth Latin Small Letter K
541 0xff4c => 0x6c, // Fullwidth Latin Small Letter L
542 0xff4d => 0x6d, // Fullwidth Latin Small Letter M
543 0xff4e => 0x6e, // Fullwidth Latin Small Letter N
544 0xff4f => 0x6f, // Fullwidth Latin Small Letter O
545 0xff50 => 0x70, // Fullwidth Latin Small Letter P
546 0xff51 => 0x71, // Fullwidth Latin Small Letter Q
547 0xff52 => 0x72, // Fullwidth Latin Small Letter R
548 0xff53 => 0x73, // Fullwidth Latin Small Letter S
549 0xff54 => 0x74, // Fullwidth Latin Small Letter T
550 0xff55 => 0x75, // Fullwidth Latin Small Letter U
551 0xff56 => 0x76, // Fullwidth Latin Small Letter V
552 0xff57 => 0x77, // Fullwidth Latin Small Letter W
553 0xff58 => 0x78, // Fullwidth Latin Small Letter X
554 0xff59 => 0x79, // Fullwidth Latin Small Letter Y
555 0xff5a => 0x7a, // Fullwidth Latin Small Letter Z
556 0xff5b => 0x7b, // Fullwidth Left Curly Bracket
557 0xff5c => 0x7c, // Fullwidth Vertical Line
558 0xff5d => 0x7d, // Fullwidth Right Curly Bracket
559 0xff5e => 0x7e, // Fullwidth Tilde
560 // Not in the best fit mapping, but RC uses these mappings too
561 0x2007 => 0xA0, // Figure Space
562 0x2008 => ' ', // Punctuation Space
563 0x2009 => ' ', // Thin Space
564 0x200A => ' ', // Hair Space
565 0x2012 => '-', // Figure Dash
566 0x2015 => '-', // Horizontal Bar
567 0x201B => '\'', // Single High-reversed-9 Quotation Mark
568 0x201F => '"', // Double High-reversed-9 Quotation Mark
569 0x202F => 0xA0, // Narrow No-Break Space
570 0x2033 => '"', // Double Prime
571 0x2036 => '"', // Reversed Double Prime
572 else => null,
573 };
574}
575
576test "windows-1252 to utf8" {
577 var buf = std.ArrayList(u8).init(std.testing.allocator);
578 defer buf.deinit();
579
580 const input_windows1252 = "\x81pqrstuvwxyz{|}~\x80\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8e\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9e\x9f\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff";
581 const expected_utf8 = "\xc2\x81pqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ";
582
583 var fbs = std.io.fixedBufferStream(input_windows1252);
584 const bytes_written = try windows1252ToUtf8Stream(buf.writer(), fbs.reader());
585
586 try std.testing.expectEqualStrings(expected_utf8, buf.items);
587 try std.testing.expectEqual(expected_utf8.len, bytes_written);
588}