| author | |
| committer | |
| log | 0c61466771ff205a955f3e5002d2a7f2449ccc78 |
| tree | 2a837dc71cb3077c538dd1bcd01b22ef233489de |
| parent | f60c24c73cc5c5894fbfb7060a70bc683c4a4ba5 |
| parent | 3f92cbef89e13c90e2c9f7051dbf5edef76fa90b |
| signature |
Lazily compile the `zig rc` subcommand and use it during `zig build-exe`, etc43 files changed, 17659 insertions(+), 16953 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 | ||
| 15 | const std = @import("std"); | |
| 16 | ||
| 17 | const AF_ICON: u32 = 1; | |
| 18 | ||
| 19 | pub fn isAnimatedIcon(reader: anytype) bool { | |
| 20 | const flags = getAniheaderFlags(reader) catch return false; | |
| 21 | return flags & AF_ICON == AF_ICON; | |
| 22 | } | |
| 23 | ||
| 24 | fn getAniheaderFlags(reader: anytype) !u32 { | |
| 25 | const riff_header = try reader.readBytesNoEof(4); | |
| 26 | if (!std.mem.eql(u8, &riff_header, "RIFF")) return error.InvalidFormat; | |
| 27 | ||
| 28 | _ = try reader.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 | |
| 48 | const ANIHEADER = extern struct { | |
| 49 | cbSizeof: u32, | |
| 50 | cFrames: u32, | |
| 51 | cSteps: u32, | |
| 52 | cx: u32, | |
| 53 | cy: u32, | |
| 54 | cBitCount: u32, | |
| 55 | cPlanes: u32, | |
| 56 | jifRate: u32, | |
| 57 | flags: u32, | |
| 58 | }; |
lib/compiler/resinator/ast.zig created+1084| ... | ... | @@ -0,0 +1,1084 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Allocator = std.mem.Allocator; | |
| 3 | const Token = @import("lex.zig").Token; | |
| 4 | const CodePage = @import("code_pages.zig").CodePage; | |
| 5 | ||
| 6 | pub const Tree = struct { | |
| 7 | node: *Node, | |
| 8 | input_code_pages: CodePageLookup, | |
| 9 | output_code_pages: CodePageLookup, | |
| 10 | ||
| 11 | /// not owned by the tree | |
| 12 | source: []const u8, | |
| 13 | ||
| 14 | arena: std.heap.ArenaAllocator.State, | |
| 15 | allocator: Allocator, | |
| 16 | ||
| 17 | pub fn deinit(self: *Tree) void { | |
| 18 | self.arena.promote(self.allocator).deinit(); | |
| 19 | } | |
| 20 | ||
| 21 | pub fn root(self: *Tree) *Node.Root { | |
| 22 | return @fieldParentPtr(Node.Root, "base", self.node); | |
| 23 | } | |
| 24 | ||
| 25 | pub fn dump(self: *Tree, writer: anytype) @TypeOf(writer).Error!void { | |
| 26 | try self.node.dump(self, writer, 0); | |
| 27 | } | |
| 28 | }; | |
| 29 | ||
| 30 | pub const CodePageLookup = struct { | |
| 31 | lookup: std.ArrayListUnmanaged(CodePage) = .{}, | |
| 32 | allocator: Allocator, | |
| 33 | default_code_page: CodePage, | |
| 34 | ||
| 35 | pub fn init(allocator: Allocator, default_code_page: CodePage) CodePageLookup { | |
| 36 | return .{ | |
| 37 | .allocator = allocator, | |
| 38 | .default_code_page = default_code_page, | |
| 39 | }; | |
| 40 | } | |
| 41 | ||
| 42 | pub fn deinit(self: *CodePageLookup) void { | |
| 43 | self.lookup.deinit(self.allocator); | |
| 44 | } | |
| 45 | ||
| 46 | /// line_num is 1-indexed | |
| 47 | pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: CodePage) !void { | |
| 48 | const index = line_num - 1; | |
| 49 | if (index >= self.lookup.items.len) { | |
| 50 | const new_size = line_num; | |
| 51 | const missing_lines_start_index = self.lookup.items.len; | |
| 52 | try self.lookup.resize(self.allocator, new_size); | |
| 53 | ||
| 54 | // If there are any gaps created, we need to fill them in with the value of the | |
| 55 | // last line before the gap. This can happen for e.g. string literals that | |
| 56 | // span multiple lines, or if the start of a file has multiple empty lines. | |
| 57 | const fill_value = if (missing_lines_start_index > 0) | |
| 58 | self.lookup.items[missing_lines_start_index - 1] | |
| 59 | else | |
| 60 | self.default_code_page; | |
| 61 | var i: usize = missing_lines_start_index; | |
| 62 | while (i < new_size - 1) : (i += 1) { | |
| 63 | self.lookup.items[i] = fill_value; | |
| 64 | } | |
| 65 | } | |
| 66 | self.lookup.items[index] = code_page; | |
| 67 | } | |
| 68 | ||
| 69 | pub fn setForToken(self: *CodePageLookup, token: Token, code_page: CodePage) !void { | |
| 70 | return self.setForLineNum(token.line_number, code_page); | |
| 71 | } | |
| 72 | ||
| 73 | /// line_num is 1-indexed | |
| 74 | pub fn getForLineNum(self: CodePageLookup, line_num: usize) CodePage { | |
| 75 | return self.lookup.items[line_num - 1]; | |
| 76 | } | |
| 77 | ||
| 78 | pub fn getForToken(self: CodePageLookup, token: Token) CodePage { | |
| 79 | return self.getForLineNum(token.line_number); | |
| 80 | } | |
| 81 | }; | |
| 82 | ||
| 83 | test "CodePageLookup" { | |
| 84 | var lookup = CodePageLookup.init(std.testing.allocator, .windows1252); | |
| 85 | defer lookup.deinit(); | |
| 86 | ||
| 87 | try lookup.setForLineNum(5, .utf8); | |
| 88 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1)); | |
| 89 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2)); | |
| 90 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3)); | |
| 91 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4)); | |
| 92 | try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5)); | |
| 93 | try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len); | |
| 94 | ||
| 95 | try lookup.setForLineNum(7, .windows1252); | |
| 96 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1)); | |
| 97 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2)); | |
| 98 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3)); | |
| 99 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4)); | |
| 100 | try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5)); | |
| 101 | try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(6)); | |
| 102 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(7)); | |
| 103 | try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len); | |
| 104 | } | |
| 105 | ||
| 106 | pub const Node = struct { | |
| 107 | id: Id, | |
| 108 | ||
| 109 | pub const Id = enum { | |
| 110 | root, | |
| 111 | resource_external, | |
| 112 | resource_raw_data, | |
| 113 | literal, | |
| 114 | binary_expression, | |
| 115 | grouped_expression, | |
| 116 | not_expression, | |
| 117 | accelerators, | |
| 118 | accelerator, | |
| 119 | dialog, | |
| 120 | control_statement, | |
| 121 | toolbar, | |
| 122 | menu, | |
| 123 | menu_item, | |
| 124 | menu_item_separator, | |
| 125 | menu_item_ex, | |
| 126 | popup, | |
| 127 | popup_ex, | |
| 128 | version_info, | |
| 129 | version_statement, | |
| 130 | block, | |
| 131 | block_value, | |
| 132 | block_value_value, | |
| 133 | string_table, | |
| 134 | string_table_string, | |
| 135 | language_statement, | |
| 136 | font_statement, | |
| 137 | simple_statement, | |
| 138 | invalid, | |
| 139 | ||
| 140 | pub fn Type(comptime id: Id) type { | |
| 141 | return switch (id) { | |
| 142 | .root => Root, | |
| 143 | .resource_external => ResourceExternal, | |
| 144 | .resource_raw_data => ResourceRawData, | |
| 145 | .literal => Literal, | |
| 146 | .binary_expression => BinaryExpression, | |
| 147 | .grouped_expression => GroupedExpression, | |
| 148 | .not_expression => NotExpression, | |
| 149 | .accelerators => Accelerators, | |
| 150 | .accelerator => Accelerator, | |
| 151 | .dialog => Dialog, | |
| 152 | .control_statement => ControlStatement, | |
| 153 | .toolbar => Toolbar, | |
| 154 | .menu => Menu, | |
| 155 | .menu_item => MenuItem, | |
| 156 | .menu_item_separator => MenuItemSeparator, | |
| 157 | .menu_item_ex => MenuItemEx, | |
| 158 | .popup => Popup, | |
| 159 | .popup_ex => PopupEx, | |
| 160 | .version_info => VersionInfo, | |
| 161 | .version_statement => VersionStatement, | |
| 162 | .block => Block, | |
| 163 | .block_value => BlockValue, | |
| 164 | .block_value_value => BlockValueValue, | |
| 165 | .string_table => StringTable, | |
| 166 | .string_table_string => StringTableString, | |
| 167 | .language_statement => LanguageStatement, | |
| 168 | .font_statement => FontStatement, | |
| 169 | .simple_statement => SimpleStatement, | |
| 170 | .invalid => Invalid, | |
| 171 | }; | |
| 172 | } | |
| 173 | }; | |
| 174 | ||
| 175 | pub fn cast(base: *Node, comptime id: Id) ?*id.Type() { | |
| 176 | if (base.id == id) { | |
| 177 | return @fieldParentPtr(id.Type(), "base", base); | |
| 178 | } | |
| 179 | return null; | |
| 180 | } | |
| 181 | ||
| 182 | pub const Root = struct { | |
| 183 | base: Node = .{ .id = .root }, | |
| 184 | body: []*Node, | |
| 185 | }; | |
| 186 | ||
| 187 | pub const ResourceExternal = struct { | |
| 188 | base: Node = .{ .id = .resource_external }, | |
| 189 | id: Token, | |
| 190 | type: Token, | |
| 191 | common_resource_attributes: []Token, | |
| 192 | filename: *Node, | |
| 193 | }; | |
| 194 | ||
| 195 | pub const ResourceRawData = struct { | |
| 196 | base: Node = .{ .id = .resource_raw_data }, | |
| 197 | id: Token, | |
| 198 | type: Token, | |
| 199 | common_resource_attributes: []Token, | |
| 200 | begin_token: Token, | |
| 201 | raw_data: []*Node, | |
| 202 | end_token: Token, | |
| 203 | }; | |
| 204 | ||
| 205 | pub const Literal = struct { | |
| 206 | base: Node = .{ .id = .literal }, | |
| 207 | token: Token, | |
| 208 | }; | |
| 209 | ||
| 210 | pub const BinaryExpression = struct { | |
| 211 | base: Node = .{ .id = .binary_expression }, | |
| 212 | operator: Token, | |
| 213 | left: *Node, | |
| 214 | right: *Node, | |
| 215 | }; | |
| 216 | ||
| 217 | pub const GroupedExpression = struct { | |
| 218 | base: Node = .{ .id = .grouped_expression }, | |
| 219 | open_token: Token, | |
| 220 | expression: *Node, | |
| 221 | close_token: Token, | |
| 222 | }; | |
| 223 | ||
| 224 | pub const NotExpression = struct { | |
| 225 | base: Node = .{ .id = .not_expression }, | |
| 226 | not_token: Token, | |
| 227 | number_token: Token, | |
| 228 | }; | |
| 229 | ||
| 230 | pub const Accelerators = struct { | |
| 231 | base: Node = .{ .id = .accelerators }, | |
| 232 | id: Token, | |
| 233 | type: Token, | |
| 234 | common_resource_attributes: []Token, | |
| 235 | optional_statements: []*Node, | |
| 236 | begin_token: Token, | |
| 237 | accelerators: []*Node, | |
| 238 | end_token: Token, | |
| 239 | }; | |
| 240 | ||
| 241 | pub const Accelerator = struct { | |
| 242 | base: Node = .{ .id = .accelerator }, | |
| 243 | event: *Node, | |
| 244 | idvalue: *Node, | |
| 245 | type_and_options: []Token, | |
| 246 | }; | |
| 247 | ||
| 248 | pub const Dialog = struct { | |
| 249 | base: Node = .{ .id = .dialog }, | |
| 250 | id: Token, | |
| 251 | type: Token, | |
| 252 | common_resource_attributes: []Token, | |
| 253 | x: *Node, | |
| 254 | y: *Node, | |
| 255 | width: *Node, | |
| 256 | height: *Node, | |
| 257 | help_id: ?*Node, | |
| 258 | optional_statements: []*Node, | |
| 259 | begin_token: Token, | |
| 260 | controls: []*Node, | |
| 261 | end_token: Token, | |
| 262 | }; | |
| 263 | ||
| 264 | pub const ControlStatement = struct { | |
| 265 | base: Node = .{ .id = .control_statement }, | |
| 266 | type: Token, | |
| 267 | text: ?Token, | |
| 268 | /// Only relevant for the user-defined CONTROL control | |
| 269 | class: ?*Node, | |
| 270 | id: *Node, | |
| 271 | x: *Node, | |
| 272 | y: *Node, | |
| 273 | width: *Node, | |
| 274 | height: *Node, | |
| 275 | style: ?*Node, | |
| 276 | exstyle: ?*Node, | |
| 277 | help_id: ?*Node, | |
| 278 | extra_data_begin: ?Token, | |
| 279 | extra_data: []*Node, | |
| 280 | extra_data_end: ?Token, | |
| 281 | ||
| 282 | /// Returns true if this node describes a user-defined CONTROL control | |
| 283 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/control-control | |
| 284 | pub fn isUserDefined(self: *const ControlStatement) bool { | |
| 285 | return self.class != null; | |
| 286 | } | |
| 287 | }; | |
| 288 | ||
| 289 | pub const Toolbar = struct { | |
| 290 | base: Node = .{ .id = .toolbar }, | |
| 291 | id: Token, | |
| 292 | type: Token, | |
| 293 | common_resource_attributes: []Token, | |
| 294 | button_width: *Node, | |
| 295 | button_height: *Node, | |
| 296 | begin_token: Token, | |
| 297 | /// Will contain Literal and SimpleStatement nodes | |
| 298 | buttons: []*Node, | |
| 299 | end_token: Token, | |
| 300 | }; | |
| 301 | ||
| 302 | pub const Menu = struct { | |
| 303 | base: Node = .{ .id = .menu }, | |
| 304 | id: Token, | |
| 305 | type: Token, | |
| 306 | common_resource_attributes: []Token, | |
| 307 | optional_statements: []*Node, | |
| 308 | /// `help_id` will never be non-null if `type` is MENU | |
| 309 | help_id: ?*Node, | |
| 310 | begin_token: Token, | |
| 311 | items: []*Node, | |
| 312 | end_token: Token, | |
| 313 | }; | |
| 314 | ||
| 315 | pub const MenuItem = struct { | |
| 316 | base: Node = .{ .id = .menu_item }, | |
| 317 | menuitem: Token, | |
| 318 | text: Token, | |
| 319 | result: *Node, | |
| 320 | option_list: []Token, | |
| 321 | }; | |
| 322 | ||
| 323 | pub const MenuItemSeparator = struct { | |
| 324 | base: Node = .{ .id = .menu_item_separator }, | |
| 325 | menuitem: Token, | |
| 326 | separator: Token, | |
| 327 | }; | |
| 328 | ||
| 329 | pub const MenuItemEx = struct { | |
| 330 | base: Node = .{ .id = .menu_item_ex }, | |
| 331 | menuitem: Token, | |
| 332 | text: Token, | |
| 333 | id: ?*Node, | |
| 334 | type: ?*Node, | |
| 335 | state: ?*Node, | |
| 336 | }; | |
| 337 | ||
| 338 | pub const Popup = struct { | |
| 339 | base: Node = .{ .id = .popup }, | |
| 340 | popup: Token, | |
| 341 | text: Token, | |
| 342 | option_list: []Token, | |
| 343 | begin_token: Token, | |
| 344 | items: []*Node, | |
| 345 | end_token: Token, | |
| 346 | }; | |
| 347 | ||
| 348 | pub const PopupEx = struct { | |
| 349 | base: Node = .{ .id = .popup_ex }, | |
| 350 | popup: Token, | |
| 351 | text: Token, | |
| 352 | id: ?*Node, | |
| 353 | type: ?*Node, | |
| 354 | state: ?*Node, | |
| 355 | help_id: ?*Node, | |
| 356 | begin_token: Token, | |
| 357 | items: []*Node, | |
| 358 | end_token: Token, | |
| 359 | }; | |
| 360 | ||
| 361 | pub const VersionInfo = struct { | |
| 362 | base: Node = .{ .id = .version_info }, | |
| 363 | id: Token, | |
| 364 | versioninfo: Token, | |
| 365 | common_resource_attributes: []Token, | |
| 366 | /// Will contain VersionStatement and/or SimpleStatement nodes | |
| 367 | fixed_info: []*Node, | |
| 368 | begin_token: Token, | |
| 369 | block_statements: []*Node, | |
| 370 | end_token: Token, | |
| 371 | }; | |
| 372 | ||
| 373 | /// Used for FILEVERSION and PRODUCTVERSION statements | |
| 374 | pub const VersionStatement = struct { | |
| 375 | base: Node = .{ .id = .version_statement }, | |
| 376 | type: Token, | |
| 377 | /// Between 1-4 parts | |
| 378 | parts: []*Node, | |
| 379 | }; | |
| 380 | ||
| 381 | pub const Block = struct { | |
| 382 | base: Node = .{ .id = .block }, | |
| 383 | /// The BLOCK token itself | |
| 384 | identifier: Token, | |
| 385 | key: Token, | |
| 386 | /// This is undocumented but BLOCK statements support values after | |
| 387 | /// the key just like VALUE statements. | |
| 388 | values: []*Node, | |
| 389 | begin_token: Token, | |
| 390 | children: []*Node, | |
| 391 | end_token: Token, | |
| 392 | }; | |
| 393 | ||
| 394 | pub const BlockValue = struct { | |
| 395 | base: Node = .{ .id = .block_value }, | |
| 396 | /// The VALUE token itself | |
| 397 | identifier: Token, | |
| 398 | key: Token, | |
| 399 | /// These will be BlockValueValue nodes | |
| 400 | values: []*Node, | |
| 401 | }; | |
| 402 | ||
| 403 | pub const BlockValueValue = struct { | |
| 404 | base: Node = .{ .id = .block_value_value }, | |
| 405 | expression: *Node, | |
| 406 | /// Whether or not the value has a trailing comma is relevant | |
| 407 | trailing_comma: bool, | |
| 408 | }; | |
| 409 | ||
| 410 | pub const StringTable = struct { | |
| 411 | base: Node = .{ .id = .string_table }, | |
| 412 | type: Token, | |
| 413 | common_resource_attributes: []Token, | |
| 414 | optional_statements: []*Node, | |
| 415 | begin_token: Token, | |
| 416 | strings: []*Node, | |
| 417 | end_token: Token, | |
| 418 | }; | |
| 419 | ||
| 420 | pub const StringTableString = struct { | |
| 421 | base: Node = .{ .id = .string_table_string }, | |
| 422 | id: *Node, | |
| 423 | maybe_comma: ?Token, | |
| 424 | string: Token, | |
| 425 | }; | |
| 426 | ||
| 427 | pub const LanguageStatement = struct { | |
| 428 | base: Node = .{ .id = .language_statement }, | |
| 429 | /// The LANGUAGE token itself | |
| 430 | language_token: Token, | |
| 431 | primary_language_id: *Node, | |
| 432 | sublanguage_id: *Node, | |
| 433 | }; | |
| 434 | ||
| 435 | pub const FontStatement = struct { | |
| 436 | base: Node = .{ .id = .font_statement }, | |
| 437 | /// The FONT token itself | |
| 438 | identifier: Token, | |
| 439 | point_size: *Node, | |
| 440 | typeface: Token, | |
| 441 | weight: ?*Node, | |
| 442 | italic: ?*Node, | |
| 443 | char_set: ?*Node, | |
| 444 | }; | |
| 445 | ||
| 446 | /// A statement with one value associated with it. | |
| 447 | /// Used for CAPTION, CHARACTERISTICS, CLASS, EXSTYLE, MENU, STYLE, VERSION, | |
| 448 | /// as well as VERSIONINFO-specific statements FILEFLAGSMASK, FILEFLAGS, FILEOS, | |
| 449 | /// FILETYPE, FILESUBTYPE | |
| 450 | pub const SimpleStatement = struct { | |
| 451 | base: Node = .{ .id = .simple_statement }, | |
| 452 | identifier: Token, | |
| 453 | value: *Node, | |
| 454 | }; | |
| 455 | ||
| 456 | pub const Invalid = struct { | |
| 457 | base: Node = .{ .id = .invalid }, | |
| 458 | context: []Token, | |
| 459 | }; | |
| 460 | ||
| 461 | pub fn isNumberExpression(node: *const Node) bool { | |
| 462 | switch (node.id) { | |
| 463 | .literal => { | |
| 464 | const literal = @fieldParentPtr(Node.Literal, "base", node); | |
| 465 | return switch (literal.token.id) { | |
| 466 | .number => true, | |
| 467 | else => false, | |
| 468 | }; | |
| 469 | }, | |
| 470 | .binary_expression, .grouped_expression, .not_expression => return true, | |
| 471 | else => return false, | |
| 472 | } | |
| 473 | } | |
| 474 | ||
| 475 | pub fn isStringLiteral(node: *const Node) bool { | |
| 476 | switch (node.id) { | |
| 477 | .literal => { | |
| 478 | const literal = @fieldParentPtr(Node.Literal, "base", node); | |
| 479 | return switch (literal.token.id) { | |
| 480 | .quoted_ascii_string, .quoted_wide_string => true, | |
| 481 | else => false, | |
| 482 | }; | |
| 483 | }, | |
| 484 | else => return false, | |
| 485 | } | |
| 486 | } | |
| 487 | ||
| 488 | pub fn getFirstToken(node: *const Node) Token { | |
| 489 | switch (node.id) { | |
| 490 | .root => unreachable, | |
| 491 | .resource_external => { | |
| 492 | const casted = @fieldParentPtr(Node.ResourceExternal, "base", node); | |
| 493 | return casted.id; | |
| 494 | }, | |
| 495 | .resource_raw_data => { | |
| 496 | const casted = @fieldParentPtr(Node.ResourceRawData, "base", node); | |
| 497 | return casted.id; | |
| 498 | }, | |
| 499 | .literal => { | |
| 500 | const casted = @fieldParentPtr(Node.Literal, "base", node); | |
| 501 | return casted.token; | |
| 502 | }, | |
| 503 | .binary_expression => { | |
| 504 | const casted = @fieldParentPtr(Node.BinaryExpression, "base", node); | |
| 505 | return casted.left.getFirstToken(); | |
| 506 | }, | |
| 507 | .grouped_expression => { | |
| 508 | const casted = @fieldParentPtr(Node.GroupedExpression, "base", node); | |
| 509 | return casted.open_token; | |
| 510 | }, | |
| 511 | .not_expression => { | |
| 512 | const casted = @fieldParentPtr(Node.NotExpression, "base", node); | |
| 513 | return casted.not_token; | |
| 514 | }, | |
| 515 | .accelerators => { | |
| 516 | const casted = @fieldParentPtr(Node.Accelerators, "base", node); | |
| 517 | return casted.id; | |
| 518 | }, | |
| 519 | .accelerator => { | |
| 520 | const casted = @fieldParentPtr(Node.Accelerator, "base", node); | |
| 521 | return casted.event.getFirstToken(); | |
| 522 | }, | |
| 523 | .dialog => { | |
| 524 | const casted = @fieldParentPtr(Node.Dialog, "base", node); | |
| 525 | return casted.id; | |
| 526 | }, | |
| 527 | .control_statement => { | |
| 528 | const casted = @fieldParentPtr(Node.ControlStatement, "base", node); | |
| 529 | return casted.type; | |
| 530 | }, | |
| 531 | .toolbar => { | |
| 532 | const casted = @fieldParentPtr(Node.Toolbar, "base", node); | |
| 533 | return casted.id; | |
| 534 | }, | |
| 535 | .menu => { | |
| 536 | const casted = @fieldParentPtr(Node.Menu, "base", node); | |
| 537 | return casted.id; | |
| 538 | }, | |
| 539 | inline .menu_item, .menu_item_separator, .menu_item_ex => |menu_item_type| { | |
| 540 | const node_type = menu_item_type.Type(); | |
| 541 | const casted = @fieldParentPtr(node_type, "base", node); | |
| 542 | return casted.menuitem; | |
| 543 | }, | |
| 544 | inline .popup, .popup_ex => |popup_type| { | |
| 545 | const node_type = popup_type.Type(); | |
| 546 | const casted = @fieldParentPtr(node_type, "base", node); | |
| 547 | return casted.popup; | |
| 548 | }, | |
| 549 | .version_info => { | |
| 550 | const casted = @fieldParentPtr(Node.VersionInfo, "base", node); | |
| 551 | return casted.id; | |
| 552 | }, | |
| 553 | .version_statement => { | |
| 554 | const casted = @fieldParentPtr(Node.VersionStatement, "base", node); | |
| 555 | return casted.type; | |
| 556 | }, | |
| 557 | .block => { | |
| 558 | const casted = @fieldParentPtr(Node.Block, "base", node); | |
| 559 | return casted.identifier; | |
| 560 | }, | |
| 561 | .block_value => { | |
| 562 | const casted = @fieldParentPtr(Node.BlockValue, "base", node); | |
| 563 | return casted.identifier; | |
| 564 | }, | |
| 565 | .block_value_value => { | |
| 566 | const casted = @fieldParentPtr(Node.BlockValueValue, "base", node); | |
| 567 | return casted.expression.getFirstToken(); | |
| 568 | }, | |
| 569 | .string_table => { | |
| 570 | const casted = @fieldParentPtr(Node.StringTable, "base", node); | |
| 571 | return casted.type; | |
| 572 | }, | |
| 573 | .string_table_string => { | |
| 574 | const casted = @fieldParentPtr(Node.StringTableString, "base", node); | |
| 575 | return casted.id.getFirstToken(); | |
| 576 | }, | |
| 577 | .language_statement => { | |
| 578 | const casted = @fieldParentPtr(Node.LanguageStatement, "base", node); | |
| 579 | return casted.language_token; | |
| 580 | }, | |
| 581 | .font_statement => { | |
| 582 | const casted = @fieldParentPtr(Node.FontStatement, "base", node); | |
| 583 | return casted.identifier; | |
| 584 | }, | |
| 585 | .simple_statement => { | |
| 586 | const casted = @fieldParentPtr(Node.SimpleStatement, "base", node); | |
| 587 | return casted.identifier; | |
| 588 | }, | |
| 589 | .invalid => { | |
| 590 | const casted = @fieldParentPtr(Node.Invalid, "base", node); | |
| 591 | return casted.context[0]; | |
| 592 | }, | |
| 593 | } | |
| 594 | } | |
| 595 | ||
| 596 | pub fn getLastToken(node: *const Node) Token { | |
| 597 | switch (node.id) { | |
| 598 | .root => unreachable, | |
| 599 | .resource_external => { | |
| 600 | const casted = @fieldParentPtr(Node.ResourceExternal, "base", node); | |
| 601 | return casted.filename.getLastToken(); | |
| 602 | }, | |
| 603 | .resource_raw_data => { | |
| 604 | const casted = @fieldParentPtr(Node.ResourceRawData, "base", node); | |
| 605 | return casted.end_token; | |
| 606 | }, | |
| 607 | .literal => { | |
| 608 | const casted = @fieldParentPtr(Node.Literal, "base", node); | |
| 609 | return casted.token; | |
| 610 | }, | |
| 611 | .binary_expression => { | |
| 612 | const casted = @fieldParentPtr(Node.BinaryExpression, "base", node); | |
| 613 | return casted.right.getLastToken(); | |
| 614 | }, | |
| 615 | .grouped_expression => { | |
| 616 | const casted = @fieldParentPtr(Node.GroupedExpression, "base", node); | |
| 617 | return casted.close_token; | |
| 618 | }, | |
| 619 | .not_expression => { | |
| 620 | const casted = @fieldParentPtr(Node.NotExpression, "base", node); | |
| 621 | return casted.number_token; | |
| 622 | }, | |
| 623 | .accelerators => { | |
| 624 | const casted = @fieldParentPtr(Node.Accelerators, "base", node); | |
| 625 | return casted.end_token; | |
| 626 | }, | |
| 627 | .accelerator => { | |
| 628 | const casted = @fieldParentPtr(Node.Accelerator, "base", node); | |
| 629 | if (casted.type_and_options.len > 0) return casted.type_and_options[casted.type_and_options.len - 1]; | |
| 630 | return casted.idvalue.getLastToken(); | |
| 631 | }, | |
| 632 | .dialog => { | |
| 633 | const casted = @fieldParentPtr(Node.Dialog, "base", node); | |
| 634 | return casted.end_token; | |
| 635 | }, | |
| 636 | .control_statement => { | |
| 637 | const casted = @fieldParentPtr(Node.ControlStatement, "base", node); | |
| 638 | if (casted.extra_data_end) |token| return token; | |
| 639 | if (casted.help_id) |help_id_node| return help_id_node.getLastToken(); | |
| 640 | if (casted.exstyle) |exstyle_node| return exstyle_node.getLastToken(); | |
| 641 | // For user-defined CONTROL controls, the style comes before 'x', but | |
| 642 | // otherwise it comes after 'height' so it could be the last token if | |
| 643 | // it's present. | |
| 644 | if (!casted.isUserDefined()) { | |
| 645 | if (casted.style) |style_node| return style_node.getLastToken(); | |
| 646 | } | |
| 647 | return casted.height.getLastToken(); | |
| 648 | }, | |
| 649 | .toolbar => { | |
| 650 | const casted = @fieldParentPtr(Node.Toolbar, "base", node); | |
| 651 | return casted.end_token; | |
| 652 | }, | |
| 653 | .menu => { | |
| 654 | const casted = @fieldParentPtr(Node.Menu, "base", node); | |
| 655 | return casted.end_token; | |
| 656 | }, | |
| 657 | .menu_item => { | |
| 658 | const casted = @fieldParentPtr(Node.MenuItem, "base", node); | |
| 659 | if (casted.option_list.len > 0) return casted.option_list[casted.option_list.len - 1]; | |
| 660 | return casted.result.getLastToken(); | |
| 661 | }, | |
| 662 | .menu_item_separator => { | |
| 663 | const casted = @fieldParentPtr(Node.MenuItemSeparator, "base", node); | |
| 664 | return casted.separator; | |
| 665 | }, | |
| 666 | .menu_item_ex => { | |
| 667 | const casted = @fieldParentPtr(Node.MenuItemEx, "base", node); | |
| 668 | if (casted.state) |state_node| return state_node.getLastToken(); | |
| 669 | if (casted.type) |type_node| return type_node.getLastToken(); | |
| 670 | if (casted.id) |id_node| return id_node.getLastToken(); | |
| 671 | return casted.text; | |
| 672 | }, | |
| 673 | inline .popup, .popup_ex => |popup_type| { | |
| 674 | const node_type = popup_type.Type(); | |
| 675 | const casted = @fieldParentPtr(node_type, "base", node); | |
| 676 | return casted.end_token; | |
| 677 | }, | |
| 678 | .version_info => { | |
| 679 | const casted = @fieldParentPtr(Node.VersionInfo, "base", node); | |
| 680 | return casted.end_token; | |
| 681 | }, | |
| 682 | .version_statement => { | |
| 683 | const casted = @fieldParentPtr(Node.VersionStatement, "base", node); | |
| 684 | return casted.parts[casted.parts.len - 1].getLastToken(); | |
| 685 | }, | |
| 686 | .block => { | |
| 687 | const casted = @fieldParentPtr(Node.Block, "base", node); | |
| 688 | return casted.end_token; | |
| 689 | }, | |
| 690 | .block_value => { | |
| 691 | const casted = @fieldParentPtr(Node.BlockValue, "base", node); | |
| 692 | if (casted.values.len > 0) return casted.values[casted.values.len - 1].getLastToken(); | |
| 693 | return casted.key; | |
| 694 | }, | |
| 695 | .block_value_value => { | |
| 696 | const casted = @fieldParentPtr(Node.BlockValueValue, "base", node); | |
| 697 | return casted.expression.getLastToken(); | |
| 698 | }, | |
| 699 | .string_table => { | |
| 700 | const casted = @fieldParentPtr(Node.StringTable, "base", node); | |
| 701 | return casted.end_token; | |
| 702 | }, | |
| 703 | .string_table_string => { | |
| 704 | const casted = @fieldParentPtr(Node.StringTableString, "base", node); | |
| 705 | return casted.string; | |
| 706 | }, | |
| 707 | .language_statement => { | |
| 708 | const casted = @fieldParentPtr(Node.LanguageStatement, "base", node); | |
| 709 | return casted.sublanguage_id.getLastToken(); | |
| 710 | }, | |
| 711 | .font_statement => { | |
| 712 | const casted = @fieldParentPtr(Node.FontStatement, "base", node); | |
| 713 | if (casted.char_set) |char_set_node| return char_set_node.getLastToken(); | |
| 714 | if (casted.italic) |italic_node| return italic_node.getLastToken(); | |
| 715 | if (casted.weight) |weight_node| return weight_node.getLastToken(); | |
| 716 | return casted.typeface; | |
| 717 | }, | |
| 718 | .simple_statement => { | |
| 719 | const casted = @fieldParentPtr(Node.SimpleStatement, "base", node); | |
| 720 | return casted.value.getLastToken(); | |
| 721 | }, | |
| 722 | .invalid => { | |
| 723 | const casted = @fieldParentPtr(Node.Invalid, "base", node); | |
| 724 | return casted.context[casted.context.len - 1]; | |
| 725 | }, | |
| 726 | } | |
| 727 | } | |
| 728 | ||
| 729 | pub fn dump( | |
| 730 | node: *const Node, | |
| 731 | tree: *const Tree, | |
| 732 | writer: anytype, | |
| 733 | indent: usize, | |
| 734 | ) @TypeOf(writer).Error!void { | |
| 735 | try writer.writeByteNTimes(' ', indent); | |
| 736 | try writer.writeAll(@tagName(node.id)); | |
| 737 | switch (node.id) { | |
| 738 | .root => { | |
| 739 | try writer.writeAll("\n"); | |
| 740 | const root = @fieldParentPtr(Node.Root, "base", node); | |
| 741 | for (root.body) |body_node| { | |
| 742 | try body_node.dump(tree, writer, indent + 1); | |
| 743 | } | |
| 744 | }, | |
| 745 | .resource_external => { | |
| 746 | const resource = @fieldParentPtr(Node.ResourceExternal, "base", node); | |
| 747 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len }); | |
| 748 | try resource.filename.dump(tree, writer, indent + 1); | |
| 749 | }, | |
| 750 | .resource_raw_data => { | |
| 751 | const resource = @fieldParentPtr(Node.ResourceRawData, "base", node); | |
| 752 | try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len }); | |
| 753 | for (resource.raw_data) |data_expression| { | |
| 754 | try data_expression.dump(tree, writer, indent + 1); | |
| 755 | } | |
| 756 | }, | |
| 757 | .literal => { | |
| 758 | const literal = @fieldParentPtr(Node.Literal, "base", node); | |
| 759 | try writer.writeAll(" "); | |
| 760 | try writer.writeAll(literal.token.slice(tree.source)); | |
| 761 | try writer.writeAll("\n"); | |
| 762 | }, | |
| 763 | .binary_expression => { | |
| 764 | const binary = @fieldParentPtr(Node.BinaryExpression, "base", node); | |
| 765 | try writer.writeAll(" "); | |
| 766 | try writer.writeAll(binary.operator.slice(tree.source)); | |
| 767 | try writer.writeAll("\n"); | |
| 768 | try binary.left.dump(tree, writer, indent + 1); | |
| 769 | try binary.right.dump(tree, writer, indent + 1); | |
| 770 | }, | |
| 771 | .grouped_expression => { | |
| 772 | const grouped = @fieldParentPtr(Node.GroupedExpression, "base", node); | |
| 773 | try writer.writeAll("\n"); | |
| 774 | try writer.writeByteNTimes(' ', indent); | |
| 775 | try writer.writeAll(grouped.open_token.slice(tree.source)); | |
| 776 | try writer.writeAll("\n"); | |
| 777 | try grouped.expression.dump(tree, writer, indent + 1); | |
| 778 | try writer.writeByteNTimes(' ', indent); | |
| 779 | try writer.writeAll(grouped.close_token.slice(tree.source)); | |
| 780 | try writer.writeAll("\n"); | |
| 781 | }, | |
| 782 | .not_expression => { | |
| 783 | const not = @fieldParentPtr(Node.NotExpression, "base", node); | |
| 784 | try writer.writeAll(" "); | |
| 785 | try writer.writeAll(not.not_token.slice(tree.source)); | |
| 786 | try writer.writeAll(" "); | |
| 787 | try writer.writeAll(not.number_token.slice(tree.source)); | |
| 788 | try writer.writeAll("\n"); | |
| 789 | }, | |
| 790 | .accelerators => { | |
| 791 | const accelerators = @fieldParentPtr(Node.Accelerators, "base", node); | |
| 792 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len }); | |
| 793 | for (accelerators.optional_statements) |statement| { | |
| 794 | try statement.dump(tree, writer, indent + 1); | |
| 795 | } | |
| 796 | try writer.writeByteNTimes(' ', indent); | |
| 797 | try writer.writeAll(accelerators.begin_token.slice(tree.source)); | |
| 798 | try writer.writeAll("\n"); | |
| 799 | for (accelerators.accelerators) |accelerator| { | |
| 800 | try accelerator.dump(tree, writer, indent + 1); | |
| 801 | } | |
| 802 | try writer.writeByteNTimes(' ', indent); | |
| 803 | try writer.writeAll(accelerators.end_token.slice(tree.source)); | |
| 804 | try writer.writeAll("\n"); | |
| 805 | }, | |
| 806 | .accelerator => { | |
| 807 | const accelerator = @fieldParentPtr(Node.Accelerator, "base", node); | |
| 808 | for (accelerator.type_and_options, 0..) |option, i| { | |
| 809 | if (i != 0) try writer.writeAll(","); | |
| 810 | try writer.writeByte(' '); | |
| 811 | try writer.writeAll(option.slice(tree.source)); | |
| 812 | } | |
| 813 | try writer.writeAll("\n"); | |
| 814 | try accelerator.event.dump(tree, writer, indent + 1); | |
| 815 | try accelerator.idvalue.dump(tree, writer, indent + 1); | |
| 816 | }, | |
| 817 | .dialog => { | |
| 818 | const dialog = @fieldParentPtr(Node.Dialog, "base", node); | |
| 819 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len }); | |
| 820 | inline for (.{ "x", "y", "width", "height" }) |arg| { | |
| 821 | try writer.writeByteNTimes(' ', indent + 1); | |
| 822 | try writer.writeAll(arg ++ ":\n"); | |
| 823 | try @field(dialog, arg).dump(tree, writer, indent + 2); | |
| 824 | } | |
| 825 | if (dialog.help_id) |help_id| { | |
| 826 | try writer.writeByteNTimes(' ', indent + 1); | |
| 827 | try writer.writeAll("help_id:\n"); | |
| 828 | try help_id.dump(tree, writer, indent + 2); | |
| 829 | } | |
| 830 | for (dialog.optional_statements) |statement| { | |
| 831 | try statement.dump(tree, writer, indent + 1); | |
| 832 | } | |
| 833 | try writer.writeByteNTimes(' ', indent); | |
| 834 | try writer.writeAll(dialog.begin_token.slice(tree.source)); | |
| 835 | try writer.writeAll("\n"); | |
| 836 | for (dialog.controls) |control| { | |
| 837 | try control.dump(tree, writer, indent + 1); | |
| 838 | } | |
| 839 | try writer.writeByteNTimes(' ', indent); | |
| 840 | try writer.writeAll(dialog.end_token.slice(tree.source)); | |
| 841 | try writer.writeAll("\n"); | |
| 842 | }, | |
| 843 | .control_statement => { | |
| 844 | const control = @fieldParentPtr(Node.ControlStatement, "base", node); | |
| 845 | try writer.print(" {s}", .{control.type.slice(tree.source)}); | |
| 846 | if (control.text) |text| { | |
| 847 | try writer.print(" text: {s}", .{text.slice(tree.source)}); | |
| 848 | } | |
| 849 | try writer.writeByte('\n'); | |
| 850 | if (control.class) |class| { | |
| 851 | try writer.writeByteNTimes(' ', indent + 1); | |
| 852 | try writer.writeAll("class:\n"); | |
| 853 | try class.dump(tree, writer, indent + 2); | |
| 854 | } | |
| 855 | inline for (.{ "id", "x", "y", "width", "height" }) |arg| { | |
| 856 | try writer.writeByteNTimes(' ', indent + 1); | |
| 857 | try writer.writeAll(arg ++ ":\n"); | |
| 858 | try @field(control, arg).dump(tree, writer, indent + 2); | |
| 859 | } | |
| 860 | inline for (.{ "style", "exstyle", "help_id" }) |arg| { | |
| 861 | if (@field(control, arg)) |val_node| { | |
| 862 | try writer.writeByteNTimes(' ', indent + 1); | |
| 863 | try writer.writeAll(arg ++ ":\n"); | |
| 864 | try val_node.dump(tree, writer, indent + 2); | |
| 865 | } | |
| 866 | } | |
| 867 | if (control.extra_data_begin != null) { | |
| 868 | try writer.writeByteNTimes(' ', indent); | |
| 869 | try writer.writeAll(control.extra_data_begin.?.slice(tree.source)); | |
| 870 | try writer.writeAll("\n"); | |
| 871 | for (control.extra_data) |data_node| { | |
| 872 | try data_node.dump(tree, writer, indent + 1); | |
| 873 | } | |
| 874 | try writer.writeByteNTimes(' ', indent); | |
| 875 | try writer.writeAll(control.extra_data_end.?.slice(tree.source)); | |
| 876 | try writer.writeAll("\n"); | |
| 877 | } | |
| 878 | }, | |
| 879 | .toolbar => { | |
| 880 | const toolbar = @fieldParentPtr(Node.Toolbar, "base", node); | |
| 881 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len }); | |
| 882 | inline for (.{ "button_width", "button_height" }) |arg| { | |
| 883 | try writer.writeByteNTimes(' ', indent + 1); | |
| 884 | try writer.writeAll(arg ++ ":\n"); | |
| 885 | try @field(toolbar, arg).dump(tree, writer, indent + 2); | |
| 886 | } | |
| 887 | try writer.writeByteNTimes(' ', indent); | |
| 888 | try writer.writeAll(toolbar.begin_token.slice(tree.source)); | |
| 889 | try writer.writeAll("\n"); | |
| 890 | for (toolbar.buttons) |button_or_sep| { | |
| 891 | try button_or_sep.dump(tree, writer, indent + 1); | |
| 892 | } | |
| 893 | try writer.writeByteNTimes(' ', indent); | |
| 894 | try writer.writeAll(toolbar.end_token.slice(tree.source)); | |
| 895 | try writer.writeAll("\n"); | |
| 896 | }, | |
| 897 | .menu => { | |
| 898 | const menu = @fieldParentPtr(Node.Menu, "base", node); | |
| 899 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len }); | |
| 900 | for (menu.optional_statements) |statement| { | |
| 901 | try statement.dump(tree, writer, indent + 1); | |
| 902 | } | |
| 903 | if (menu.help_id) |help_id| { | |
| 904 | try writer.writeByteNTimes(' ', indent + 1); | |
| 905 | try writer.writeAll("help_id:\n"); | |
| 906 | try help_id.dump(tree, writer, indent + 2); | |
| 907 | } | |
| 908 | try writer.writeByteNTimes(' ', indent); | |
| 909 | try writer.writeAll(menu.begin_token.slice(tree.source)); | |
| 910 | try writer.writeAll("\n"); | |
| 911 | for (menu.items) |item| { | |
| 912 | try item.dump(tree, writer, indent + 1); | |
| 913 | } | |
| 914 | try writer.writeByteNTimes(' ', indent); | |
| 915 | try writer.writeAll(menu.end_token.slice(tree.source)); | |
| 916 | try writer.writeAll("\n"); | |
| 917 | }, | |
| 918 | .menu_item => { | |
| 919 | const menu_item = @fieldParentPtr(Node.MenuItem, "base", node); | |
| 920 | try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len }); | |
| 921 | try menu_item.result.dump(tree, writer, indent + 1); | |
| 922 | }, | |
| 923 | .menu_item_separator => { | |
| 924 | const menu_item = @fieldParentPtr(Node.MenuItemSeparator, "base", node); | |
| 925 | try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) }); | |
| 926 | }, | |
| 927 | .menu_item_ex => { | |
| 928 | const menu_item = @fieldParentPtr(Node.MenuItemEx, "base", node); | |
| 929 | try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) }); | |
| 930 | inline for (.{ "id", "type", "state" }) |arg| { | |
| 931 | if (@field(menu_item, arg)) |val_node| { | |
| 932 | try writer.writeByteNTimes(' ', indent + 1); | |
| 933 | try writer.writeAll(arg ++ ":\n"); | |
| 934 | try val_node.dump(tree, writer, indent + 2); | |
| 935 | } | |
| 936 | } | |
| 937 | }, | |
| 938 | .popup => { | |
| 939 | const popup = @fieldParentPtr(Node.Popup, "base", node); | |
| 940 | try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len }); | |
| 941 | try writer.writeByteNTimes(' ', indent); | |
| 942 | try writer.writeAll(popup.begin_token.slice(tree.source)); | |
| 943 | try writer.writeAll("\n"); | |
| 944 | for (popup.items) |item| { | |
| 945 | try item.dump(tree, writer, indent + 1); | |
| 946 | } | |
| 947 | try writer.writeByteNTimes(' ', indent); | |
| 948 | try writer.writeAll(popup.end_token.slice(tree.source)); | |
| 949 | try writer.writeAll("\n"); | |
| 950 | }, | |
| 951 | .popup_ex => { | |
| 952 | const popup = @fieldParentPtr(Node.PopupEx, "base", node); | |
| 953 | try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) }); | |
| 954 | inline for (.{ "id", "type", "state", "help_id" }) |arg| { | |
| 955 | if (@field(popup, arg)) |val_node| { | |
| 956 | try writer.writeByteNTimes(' ', indent + 1); | |
| 957 | try writer.writeAll(arg ++ ":\n"); | |
| 958 | try val_node.dump(tree, writer, indent + 2); | |
| 959 | } | |
| 960 | } | |
| 961 | try writer.writeByteNTimes(' ', indent); | |
| 962 | try writer.writeAll(popup.begin_token.slice(tree.source)); | |
| 963 | try writer.writeAll("\n"); | |
| 964 | for (popup.items) |item| { | |
| 965 | try item.dump(tree, writer, indent + 1); | |
| 966 | } | |
| 967 | try writer.writeByteNTimes(' ', indent); | |
| 968 | try writer.writeAll(popup.end_token.slice(tree.source)); | |
| 969 | try writer.writeAll("\n"); | |
| 970 | }, | |
| 971 | .version_info => { | |
| 972 | const version_info = @fieldParentPtr(Node.VersionInfo, "base", node); | |
| 973 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len }); | |
| 974 | for (version_info.fixed_info) |fixed_info| { | |
| 975 | try fixed_info.dump(tree, writer, indent + 1); | |
| 976 | } | |
| 977 | try writer.writeByteNTimes(' ', indent); | |
| 978 | try writer.writeAll(version_info.begin_token.slice(tree.source)); | |
| 979 | try writer.writeAll("\n"); | |
| 980 | for (version_info.block_statements) |block| { | |
| 981 | try block.dump(tree, writer, indent + 1); | |
| 982 | } | |
| 983 | try writer.writeByteNTimes(' ', indent); | |
| 984 | try writer.writeAll(version_info.end_token.slice(tree.source)); | |
| 985 | try writer.writeAll("\n"); | |
| 986 | }, | |
| 987 | .version_statement => { | |
| 988 | const version_statement = @fieldParentPtr(Node.VersionStatement, "base", node); | |
| 989 | try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)}); | |
| 990 | for (version_statement.parts) |part| { | |
| 991 | try part.dump(tree, writer, indent + 1); | |
| 992 | } | |
| 993 | }, | |
| 994 | .block => { | |
| 995 | const block = @fieldParentPtr(Node.Block, "base", node); | |
| 996 | try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) }); | |
| 997 | for (block.values) |value| { | |
| 998 | try value.dump(tree, writer, indent + 1); | |
| 999 | } | |
| 1000 | try writer.writeByteNTimes(' ', indent); | |
| 1001 | try writer.writeAll(block.begin_token.slice(tree.source)); | |
| 1002 | try writer.writeAll("\n"); | |
| 1003 | for (block.children) |child| { | |
| 1004 | try child.dump(tree, writer, indent + 1); | |
| 1005 | } | |
| 1006 | try writer.writeByteNTimes(' ', indent); | |
| 1007 | try writer.writeAll(block.end_token.slice(tree.source)); | |
| 1008 | try writer.writeAll("\n"); | |
| 1009 | }, | |
| 1010 | .block_value => { | |
| 1011 | const block_value = @fieldParentPtr(Node.BlockValue, "base", node); | |
| 1012 | try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) }); | |
| 1013 | for (block_value.values) |value| { | |
| 1014 | try value.dump(tree, writer, indent + 1); | |
| 1015 | } | |
| 1016 | }, | |
| 1017 | .block_value_value => { | |
| 1018 | const block_value = @fieldParentPtr(Node.BlockValueValue, "base", node); | |
| 1019 | if (block_value.trailing_comma) { | |
| 1020 | try writer.writeAll(" ,"); | |
| 1021 | } | |
| 1022 | try writer.writeAll("\n"); | |
| 1023 | try block_value.expression.dump(tree, writer, indent + 1); | |
| 1024 | }, | |
| 1025 | .string_table => { | |
| 1026 | const string_table = @fieldParentPtr(Node.StringTable, "base", node); | |
| 1027 | try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len }); | |
| 1028 | for (string_table.optional_statements) |statement| { | |
| 1029 | try statement.dump(tree, writer, indent + 1); | |
| 1030 | } | |
| 1031 | try writer.writeByteNTimes(' ', indent); | |
| 1032 | try writer.writeAll(string_table.begin_token.slice(tree.source)); | |
| 1033 | try writer.writeAll("\n"); | |
| 1034 | for (string_table.strings) |string| { | |
| 1035 | try string.dump(tree, writer, indent + 1); | |
| 1036 | } | |
| 1037 | try writer.writeByteNTimes(' ', indent); | |
| 1038 | try writer.writeAll(string_table.end_token.slice(tree.source)); | |
| 1039 | try writer.writeAll("\n"); | |
| 1040 | }, | |
| 1041 | .string_table_string => { | |
| 1042 | try writer.writeAll("\n"); | |
| 1043 | const string = @fieldParentPtr(Node.StringTableString, "base", node); | |
| 1044 | try string.id.dump(tree, writer, indent + 1); | |
| 1045 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1046 | try writer.print("{s}\n", .{string.string.slice(tree.source)}); | |
| 1047 | }, | |
| 1048 | .language_statement => { | |
| 1049 | const language = @fieldParentPtr(Node.LanguageStatement, "base", node); | |
| 1050 | try writer.print(" {s}\n", .{language.language_token.slice(tree.source)}); | |
| 1051 | try language.primary_language_id.dump(tree, writer, indent + 1); | |
| 1052 | try language.sublanguage_id.dump(tree, writer, indent + 1); | |
| 1053 | }, | |
| 1054 | .font_statement => { | |
| 1055 | const font = @fieldParentPtr(Node.FontStatement, "base", node); | |
| 1056 | try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) }); | |
| 1057 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1058 | try writer.writeAll("point_size:\n"); | |
| 1059 | try font.point_size.dump(tree, writer, indent + 2); | |
| 1060 | inline for (.{ "weight", "italic", "char_set" }) |arg| { | |
| 1061 | if (@field(font, arg)) |arg_node| { | |
| 1062 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1063 | try writer.writeAll(arg ++ ":\n"); | |
| 1064 | try arg_node.dump(tree, writer, indent + 2); | |
| 1065 | } | |
| 1066 | } | |
| 1067 | }, | |
| 1068 | .simple_statement => { | |
| 1069 | const statement = @fieldParentPtr(Node.SimpleStatement, "base", node); | |
| 1070 | try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)}); | |
| 1071 | try statement.value.dump(tree, writer, indent + 1); | |
| 1072 | }, | |
| 1073 | .invalid => { | |
| 1074 | const invalid = @fieldParentPtr(Node.Invalid, "base", node); | |
| 1075 | try writer.print(" context.len: {}\n", .{invalid.context.len}); | |
| 1076 | for (invalid.context) |context_token| { | |
| 1077 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1078 | try writer.print("{s}:{s}", .{ @tagName(context_token.id), context_token.slice(tree.source) }); | |
| 1079 | try writer.writeByte('\n'); | |
| 1080 | } | |
| 1081 | }, | |
| 1082 | } | |
| 1083 | } | |
| 1084 | }; |
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 | ||
| 21 | const std = @import("std"); | |
| 22 | const BitmapHeader = @import("ico.zig").BitmapHeader; | |
| 23 | const builtin = @import("builtin"); | |
| 24 | const native_endian = builtin.cpu.arch.endian(); | |
| 25 | ||
| 26 | pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian); | |
| 27 | pub const file_header_len = 14; | |
| 28 | ||
| 29 | pub const ReadError = error{ | |
| 30 | UnexpectedEOF, | |
| 31 | InvalidFileHeader, | |
| 32 | ImpossiblePixelDataOffset, | |
| 33 | UnknownBitmapVersion, | |
| 34 | InvalidBitsPerPixel, | |
| 35 | TooManyColorsInPalette, | |
| 36 | MissingBitfieldMasks, | |
| 37 | }; | |
| 38 | ||
| 39 | pub 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 | ||
| 90 | pub 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 | |
| 145 | pub 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 | |
| 154 | pub 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 | ||
| 212 | pub 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 | ||
| 226 | fn 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 | ||
| 232 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const CodePage = @import("code_pages.zig").CodePage; | |
| 3 | const lang = @import("lang.zig"); | |
| 4 | const res = @import("res.zig"); | |
| 5 | const Allocator = std.mem.Allocator; | |
| 6 | const lex = @import("lex.zig"); | |
| 7 | ||
| 8 | /// This is what /SL 100 will set the maximum string literal length to | |
| 9 | pub const max_string_literal_length_100_percent = 8192; | |
| 10 | ||
| 11 | pub const usage_string_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 | ||
| 66 | pub 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 | ||
| 72 | pub 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 | ||
| 131 | pub 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 | ||
| 312 | pub 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 | ||
| 402 | pub 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. | |
| 406 | pub 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 | |
| 973 | pub 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 | |
| 988 | pub 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 | ||
| 1030 | test 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 | ||
| 1043 | pub 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 | ||
| 1149 | fn testParse(args: []const []const u8) !Options { | |
| 1150 | return (try testParseOutput(args, "")).?; | |
| 1151 | } | |
| 1152 | ||
| 1153 | fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options { | |
| 1154 | return (try testParseOutput(args, expected_output)).?; | |
| 1155 | } | |
| 1156 | ||
| 1157 | fn 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 | ||
| 1166 | fn 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 | ||
| 1188 | test "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 | ||
| 1274 | test "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 | ||
| 1289 | test "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 | ||
| 1327 | test "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 | ||
| 1384 | test "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 | ||
| 1427 | test "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 | ||
| 1448 | test "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 | ||
| 1487 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const windows1252 = @import("windows1252.zig"); | |
| 3 | ||
| 4 | // TODO: Parts of this comment block may be more relevant to string/NameOrOrdinal parsing | |
| 5 | // than it is to the stuff in this file. | |
| 6 | // | |
| 7 | // ‰ representations for context: | |
| 8 | // Win-1252 89 | |
| 9 | // UTF-8 E2 80 B0 | |
| 10 | // UTF-16 20 30 | |
| 11 | // | |
| 12 | // With code page 65001: | |
| 13 | // ‰ RCDATA { "‰" L"‰" } | |
| 14 | // File encoded as Windows-1252: | |
| 15 | // ‰ => <U+FFFD REPLACEMENT CHARACTER> as u16 | |
| 16 | // "‰" => 0x3F ('?') | |
| 17 | // L"‰" => <U+FFFD REPLACEMENT CHARACTER> as u16 | |
| 18 | // File encoded as UTF-8: | |
| 19 | // ‰ => <U+2030 ‰> as u16 | |
| 20 | // "‰" => 0x89 ('‰' encoded as Windows-1252) | |
| 21 | // L"‰" => <U+2030 ‰> as u16 | |
| 22 | // | |
| 23 | // With code page 1252: | |
| 24 | // ‰ RCDATA { "‰" L"‰" } | |
| 25 | // File encoded as Windows-1252: | |
| 26 | // ‰ => <U+2030 ‰> as u16 | |
| 27 | // "‰" => 0x89 ('‰' encoded as Windows-1252) | |
| 28 | // L"‰" => <U+2030 ‰> as u16 | |
| 29 | // File encoded as UTF-8: | |
| 30 | // ‰ => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 | |
| 31 | // ^ first byte of utf8 representation | |
| 32 | // ^ second byte of UTF-8 representation (0x80), but interpretted as | |
| 33 | // Windows-1252 ('€') and then converted to UTF-16 (<U+20AC>) | |
| 34 | // ^ third byte of utf8 representation | |
| 35 | // "‰" => 0xE2, 0x80, 0xB0 (the bytes of the UTF-8 representation) | |
| 36 | // L"‰" => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 (see '‰ =>' explanation) | |
| 37 | // | |
| 38 | // With code page 1252: | |
| 39 | // <0x90> RCDATA { "<0x90>" L"<0x90>" } | |
| 40 | // File encoded as Windows-1252: | |
| 41 | // <0x90> => 0x90 as u16 | |
| 42 | // "<0x90>" => 0x90 | |
| 43 | // L"<0x90>" => 0x90 as u16 | |
| 44 | // File encoded as UTF-8: | |
| 45 | // <0x90> => 0xC2 as u16, 0x90 as u16 | |
| 46 | // "<0x90>" => 0xC2, 0x90 (the bytes of the UTF-8 representation of <U+0090>) | |
| 47 | // L"<0x90>" => 0xC2 as u16, 0x90 as u16 | |
| 48 | // | |
| 49 | // Within a raw data block, file encoded as Windows-1252 (Â is <0xC2>): | |
| 50 | // "Âa" L"Âa" "\xC2ad" L"\xC2AD" | |
| 51 | // With code page 1252: | |
| 52 | // C2 61 C2 00 61 00 C2 61 64 AD C2 | |
| 53 | // Â^ a^ Â~~~^ a~~~^ .^ a^ d^ ^~~~~\xC2AD | |
| 54 | // \xC2~` | |
| 55 | // With code page 65001: | |
| 56 | // 3F 61 FD FF 61 00 C2 61 64 AD C2 | |
| 57 | // ^. a^ ^~~~. a~~~^ ^. a^ d^ ^~~~~\xC2AD | |
| 58 | // `. `. `~\xC2 | |
| 59 | // `. `.~<0xC2>a is not well-formed UTF-8 (0xC2 expects a continutation byte after it). | |
| 60 | // `. Because 'a' is a valid first byte of a UTF-8 sequence, it is not included in the | |
| 61 | // `. invalid sequence so only the <0xC2> gets converted to <U+FFFD>. | |
| 62 | // `~Same as ^ but converted to '?' instead. | |
| 63 | // | |
| 64 | // Within a raw data block, file encoded as Windows-1252 (ð is <0xF0>, € is <0x80>): | |
| 65 | // "ð€a" L"ð€a" | |
| 66 | // With code page 1252: | |
| 67 | // F0 80 61 F0 00 AC 20 61 00 | |
| 68 | // ð^ €^ a^ ð~~~^ €~~~^ a~~~^ | |
| 69 | // With code page 65001: | |
| 70 | // 3F 61 FD FF 61 00 | |
| 71 | // ^. a^ ^~~~. a~~~^ | |
| 72 | // `. `. | |
| 73 | // `. `.~<0xF0><0x80> is not well-formed UTF-8, and <0x80> is not a valid first byte, so | |
| 74 | // `. both bytes are considered an invalid sequence and get converted to '<U+FFFD>' | |
| 75 | // `~Same as ^ but converted to '?' instead. | |
| 76 | ||
| 77 | /// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers | |
| 78 | pub const CodePage = enum(u16) { | |
| 79 | // supported | |
| 80 | windows1252 = 1252, // windows-1252 ANSI Latin 1; Western European (Windows) | |
| 81 | utf8 = 65001, // utf-8 Unicode (UTF-8) | |
| 82 | ||
| 83 | // unsupported but valid | |
| 84 | ibm037 = 37, // IBM037 IBM EBCDIC US-Canada | |
| 85 | ibm437 = 437, // IBM437 OEM United States | |
| 86 | ibm500 = 500, // IBM500 IBM EBCDIC International | |
| 87 | asmo708 = 708, // ASMO-708 Arabic (ASMO 708) | |
| 88 | asmo449plus = 709, // Arabic (ASMO-449+, BCON V4) | |
| 89 | transparent_arabic = 710, // Arabic - Transparent Arabic | |
| 90 | dos720 = 720, // DOS-720 Arabic (Transparent ASMO); Arabic (DOS) | |
| 91 | ibm737 = 737, // ibm737 OEM Greek (formerly 437G); Greek (DOS) | |
| 92 | ibm775 = 775, // ibm775 OEM Baltic; Baltic (DOS) | |
| 93 | ibm850 = 850, // ibm850 OEM Multilingual Latin 1; Western European (DOS) | |
| 94 | ibm852 = 852, // ibm852 OEM Latin 2; Central European (DOS) | |
| 95 | ibm855 = 855, // IBM855 OEM Cyrillic (primarily Russian) | |
| 96 | ibm857 = 857, // ibm857 OEM Turkish; Turkish (DOS) | |
| 97 | ibm00858 = 858, // IBM00858 OEM Multilingual Latin 1 + Euro symbol | |
| 98 | ibm860 = 860, // IBM860 OEM Portuguese; Portuguese (DOS) | |
| 99 | ibm861 = 861, // ibm861 OEM Icelandic; Icelandic (DOS) | |
| 100 | dos862 = 862, // DOS-862 OEM Hebrew; Hebrew (DOS) | |
| 101 | ibm863 = 863, // IBM863 OEM French Canadian; French Canadian (DOS) | |
| 102 | ibm864 = 864, // IBM864 OEM Arabic; Arabic (864) | |
| 103 | ibm865 = 865, // IBM865 OEM Nordic; Nordic (DOS) | |
| 104 | cp866 = 866, // cp866 OEM Russian; Cyrillic (DOS) | |
| 105 | ibm869 = 869, // ibm869 OEM Modern Greek; Greek, Modern (DOS) | |
| 106 | ibm870 = 870, // IBM870 IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2 | |
| 107 | windows874 = 874, // windows-874 Thai (Windows) | |
| 108 | cp875 = 875, // cp875 IBM EBCDIC Greek Modern | |
| 109 | shift_jis = 932, // shift_jis ANSI/OEM Japanese; Japanese (Shift-JIS) | |
| 110 | gb2312 = 936, // gb2312 ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312) | |
| 111 | ks_c_5601_1987 = 949, // ks_c_5601-1987 ANSI/OEM Korean (Unified Hangul Code) | |
| 112 | big5 = 950, // big5 ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5) | |
| 113 | ibm1026 = 1026, // IBM1026 IBM EBCDIC Turkish (Latin 5) | |
| 114 | ibm01047 = 1047, // IBM01047 IBM EBCDIC Latin 1/Open System | |
| 115 | ibm01140 = 1140, // IBM01140 IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro) | |
| 116 | ibm01141 = 1141, // IBM01141 IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro) | |
| 117 | ibm01142 = 1142, // IBM01142 IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro) | |
| 118 | ibm01143 = 1143, // IBM01143 IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro) | |
| 119 | ibm01144 = 1144, // IBM01144 IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro) | |
| 120 | ibm01145 = 1145, // IBM01145 IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro) | |
| 121 | ibm01146 = 1146, // IBM01146 IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro) | |
| 122 | ibm01147 = 1147, // IBM01147 IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro) | |
| 123 | ibm01148 = 1148, // IBM01148 IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro) | |
| 124 | ibm01149 = 1149, // IBM01149 IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro) | |
| 125 | utf16 = 1200, // utf-16 Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications | |
| 126 | utf16_fffe = 1201, // unicodeFFFE Unicode UTF-16, big endian byte order; available only to managed applications | |
| 127 | windows1250 = 1250, // windows-1250 ANSI Central European; Central European (Windows) | |
| 128 | windows1251 = 1251, // windows-1251 ANSI Cyrillic; Cyrillic (Windows) | |
| 129 | windows1253 = 1253, // windows-1253 ANSI Greek; Greek (Windows) | |
| 130 | windows1254 = 1254, // windows-1254 ANSI Turkish; Turkish (Windows) | |
| 131 | windows1255 = 1255, // windows-1255 ANSI Hebrew; Hebrew (Windows) | |
| 132 | windows1256 = 1256, // windows-1256 ANSI Arabic; Arabic (Windows) | |
| 133 | windows1257 = 1257, // windows-1257 ANSI Baltic; Baltic (Windows) | |
| 134 | windows1258 = 1258, // windows-1258 ANSI/OEM Vietnamese; Vietnamese (Windows) | |
| 135 | johab = 1361, // Johab Korean (Johab) | |
| 136 | macintosh = 10000, // macintosh MAC Roman; Western European (Mac) | |
| 137 | x_mac_japanese = 10001, // x-mac-japanese Japanese (Mac) | |
| 138 | x_mac_chinesetrad = 10002, // x-mac-chinesetrad MAC Traditional Chinese (Big5); Chinese Traditional (Mac) | |
| 139 | x_mac_korean = 10003, // x-mac-korean Korean (Mac) | |
| 140 | x_mac_arabic = 10004, // x-mac-arabic Arabic (Mac) | |
| 141 | x_mac_hebrew = 10005, // x-mac-hebrew Hebrew (Mac) | |
| 142 | x_mac_greek = 10006, // x-mac-greek Greek (Mac) | |
| 143 | x_mac_cyrillic = 10007, // x-mac-cyrillic Cyrillic (Mac) | |
| 144 | x_mac_chinesesimp = 10008, // x-mac-chinesesimp MAC Simplified Chinese (GB 2312); Chinese Simplified (Mac) | |
| 145 | x_mac_romanian = 10010, // x-mac-romanian Romanian (Mac) | |
| 146 | x_mac_ukranian = 10017, // x-mac-ukrainian Ukrainian (Mac) | |
| 147 | x_mac_thai = 10021, // x-mac-thai Thai (Mac) | |
| 148 | x_mac_ce = 10029, // x-mac-ce MAC Latin 2; Central European (Mac) | |
| 149 | x_mac_icelandic = 10079, // x-mac-icelandic Icelandic (Mac) | |
| 150 | x_mac_turkish = 10081, // x-mac-turkish Turkish (Mac) | |
| 151 | x_mac_croatian = 10082, // x-mac-croatian Croatian (Mac) | |
| 152 | utf32 = 12000, // utf-32 Unicode UTF-32, little endian byte order; available only to managed applications | |
| 153 | utf32_be = 12001, // utf-32BE Unicode UTF-32, big endian byte order; available only to managed applications | |
| 154 | x_chinese_cns = 20000, // x-Chinese_CNS CNS Taiwan; Chinese Traditional (CNS) | |
| 155 | x_cp20001 = 20001, // x-cp20001 TCA Taiwan | |
| 156 | x_chinese_eten = 20002, // x_Chinese-Eten Eten Taiwan; Chinese Traditional (Eten) | |
| 157 | x_cp20003 = 20003, // x-cp20003 IBM5550 Taiwan | |
| 158 | x_cp20004 = 20004, // x-cp20004 TeleText Taiwan | |
| 159 | x_cp20005 = 20005, // x-cp20005 Wang Taiwan | |
| 160 | x_ia5 = 20105, // x-IA5 IA5 (IRV International Alphabet No. 5, 7-bit); Western European (IA5) | |
| 161 | x_ia5_german = 20106, // x-IA5-German IA5 German (7-bit) | |
| 162 | x_ia5_swedish = 20107, // x-IA5-Swedish IA5 Swedish (7-bit) | |
| 163 | x_ia5_norwegian = 20108, // x-IA5-Norwegian IA5 Norwegian (7-bit) | |
| 164 | us_ascii = 20127, // us-ascii US-ASCII (7-bit) | |
| 165 | x_cp20261 = 20261, // x-cp20261 T.61 | |
| 166 | x_cp20269 = 20269, // x-cp20269 ISO 6937 Non-Spacing Accent | |
| 167 | ibm273 = 20273, // IBM273 IBM EBCDIC Germany | |
| 168 | ibm277 = 20277, // IBM277 IBM EBCDIC Denmark-Norway | |
| 169 | ibm278 = 20278, // IBM278 IBM EBCDIC Finland-Sweden | |
| 170 | ibm280 = 20280, // IBM280 IBM EBCDIC Italy | |
| 171 | ibm284 = 20284, // IBM284 IBM EBCDIC Latin America-Spain | |
| 172 | ibm285 = 20285, // IBM285 IBM EBCDIC United Kingdom | |
| 173 | ibm290 = 20290, // IBM290 IBM EBCDIC Japanese Katakana Extended | |
| 174 | ibm297 = 20297, // IBM297 IBM EBCDIC France | |
| 175 | ibm420 = 20420, // IBM420 IBM EBCDIC Arabic | |
| 176 | ibm423 = 20423, // IBM423 IBM EBCDIC Greek | |
| 177 | ibm424 = 20424, // IBM424 IBM EBCDIC Hebrew | |
| 178 | x_ebcdic_korean_extended = 20833, // x-EBCDIC-KoreanExtended IBM EBCDIC Korean Extended | |
| 179 | ibm_thai = 20838, // IBM-Thai IBM EBCDIC Thai | |
| 180 | koi8_r = 20866, // koi8-r Russian (KOI8-R); Cyrillic (KOI8-R) | |
| 181 | ibm871 = 20871, // IBM871 IBM EBCDIC Icelandic | |
| 182 | ibm880 = 20880, // IBM880 IBM EBCDIC Cyrillic Russian | |
| 183 | ibm905 = 20905, // IBM905 IBM EBCDIC Turkish | |
| 184 | ibm00924 = 20924, // IBM00924 IBM EBCDIC Latin 1/Open System (1047 + Euro symbol) | |
| 185 | euc_jp_jis = 20932, // EUC-JP Japanese (JIS 0208-1990 and 0212-1990) | |
| 186 | x_cp20936 = 20936, // x-cp20936 Simplified Chinese (GB2312); Chinese Simplified (GB2312-80) | |
| 187 | x_cp20949 = 20949, // x-cp20949 Korean Wansung | |
| 188 | cp1025 = 21025, // cp1025 IBM EBCDIC Cyrillic Serbian-Bulgarian | |
| 189 | // = 21027, // (deprecated) | |
| 190 | koi8_u = 21866, // koi8-u Ukrainian (KOI8-U); Cyrillic (KOI8-U) | |
| 191 | iso8859_1 = 28591, // iso-8859-1 ISO 8859-1 Latin 1; Western European (ISO) | |
| 192 | iso8859_2 = 28592, // iso-8859-2 ISO 8859-2 Central European; Central European (ISO) | |
| 193 | iso8859_3 = 28593, // iso-8859-3 ISO 8859-3 Latin 3 | |
| 194 | iso8859_4 = 28594, // iso-8859-4 ISO 8859-4 Baltic | |
| 195 | iso8859_5 = 28595, // iso-8859-5 ISO 8859-5 Cyrillic | |
| 196 | iso8859_6 = 28596, // iso-8859-6 ISO 8859-6 Arabic | |
| 197 | iso8859_7 = 28597, // iso-8859-7 ISO 8859-7 Greek | |
| 198 | iso8859_8 = 28598, // iso-8859-8 ISO 8859-8 Hebrew; Hebrew (ISO-Visual) | |
| 199 | iso8859_9 = 28599, // iso-8859-9 ISO 8859-9 Turkish | |
| 200 | iso8859_13 = 28603, // iso-8859-13 ISO 8859-13 Estonian | |
| 201 | iso8859_15 = 28605, // iso-8859-15 ISO 8859-15 Latin 9 | |
| 202 | x_europa = 29001, // x-Europa Europa 3 | |
| 203 | is8859_8_i = 38598, // iso-8859-8-i ISO 8859-8 Hebrew; Hebrew (ISO-Logical) | |
| 204 | iso2022_jp = 50220, // iso-2022-jp ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS) | |
| 205 | cs_iso2022_jp = 50221, // csISO2022JP ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana) | |
| 206 | iso2022_jp_jis_x = 50222, // iso-2022-jp ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI) | |
| 207 | iso2022_kr = 50225, // iso-2022-kr ISO 2022 Korean | |
| 208 | x_cp50227 = 50227, // x-cp50227 ISO 2022 Simplified Chinese; Chinese Simplified (ISO 2022) | |
| 209 | iso2022_chinesetrad = 50229, // ISO 2022 Traditional Chinese | |
| 210 | ebcdic_jp_katakana_extended = 50930, // EBCDIC Japanese (Katakana) Extended | |
| 211 | ebcdic_us_ca_jp = 50931, // EBCDIC US-Canada and Japanese | |
| 212 | ebcdic_kr_extended = 50933, // EBCDIC Korean Extended and Korean | |
| 213 | ebcdic_chinesesimp_extended = 50935, // EBCDIC Simplified Chinese Extended and Simplified Chinese | |
| 214 | ebcdic_chinesesimp = 50936, // EBCDIC Simplified Chinese | |
| 215 | ebcdic_us_ca_chinesetrad = 50937, // EBCDIC US-Canada and Traditional Chinese | |
| 216 | ebcdic_jp_latin_extended = 50939, // EBCDIC Japanese (Latin) Extended and Japanese | |
| 217 | euc_jp = 51932, // euc-jp EUC Japanese | |
| 218 | euc_cn = 51936, // EUC-CN EUC Simplified Chinese; Chinese Simplified (EUC) | |
| 219 | euc_kr = 51949, // euc-kr EUC Korean | |
| 220 | euc_chinesetrad = 51950, // EUC Traditional Chinese | |
| 221 | hz_gb2312 = 52936, // hz-gb-2312 HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ) | |
| 222 | gb18030 = 54936, // GB18030 Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030) | |
| 223 | x_iscii_de = 57002, // x-iscii-de ISCII Devanagari | |
| 224 | x_iscii_be = 57003, // x-iscii-be ISCII Bangla | |
| 225 | x_iscii_ta = 57004, // x-iscii-ta ISCII Tamil | |
| 226 | x_iscii_te = 57005, // x-iscii-te ISCII Telugu | |
| 227 | x_iscii_as = 57006, // x-iscii-as ISCII Assamese | |
| 228 | x_iscii_or = 57007, // x-iscii-or ISCII Odia | |
| 229 | x_iscii_ka = 57008, // x-iscii-ka ISCII Kannada | |
| 230 | x_iscii_ma = 57009, // x-iscii-ma ISCII Malayalam | |
| 231 | x_iscii_gu = 57010, // x-iscii-gu ISCII Gujarati | |
| 232 | x_iscii_pa = 57011, // x-iscii-pa ISCII Punjabi | |
| 233 | utf7 = 65000, // utf-7 Unicode (UTF-7) | |
| 234 | ||
| 235 | pub fn codepointAt(code_page: CodePage, index: usize, bytes: []const u8) ?Codepoint { | |
| 236 | if (index >= bytes.len) return null; | |
| 237 | switch (code_page) { | |
| 238 | .windows1252 => { | |
| 239 | // All byte values have a representation, so just convert the byte | |
| 240 | return Codepoint{ | |
| 241 | .value = windows1252.toCodepoint(bytes[index]), | |
| 242 | .byte_len = 1, | |
| 243 | }; | |
| 244 | }, | |
| 245 | .utf8 => { | |
| 246 | return Utf8.WellFormedDecoder.decode(bytes[index..]); | |
| 247 | }, | |
| 248 | else => unreachable, | |
| 249 | } | |
| 250 | } | |
| 251 | ||
| 252 | pub fn isSupported(code_page: CodePage) bool { | |
| 253 | return switch (code_page) { | |
| 254 | .windows1252, .utf8 => true, | |
| 255 | else => false, | |
| 256 | }; | |
| 257 | } | |
| 258 | ||
| 259 | pub fn getByIdentifier(identifier: u16) !CodePage { | |
| 260 | // There's probably a more efficient way to do this (e.g. ComptimeHashMap?) but | |
| 261 | // this should be fine, especially since this function likely won't be called much. | |
| 262 | inline for (@typeInfo(CodePage).Enum.fields) |enumField| { | |
| 263 | if (identifier == enumField.value) { | |
| 264 | return @field(CodePage, enumField.name); | |
| 265 | } | |
| 266 | } | |
| 267 | return error.InvalidCodePage; | |
| 268 | } | |
| 269 | ||
| 270 | pub fn getByIdentifierEnsureSupported(identifier: u16) !CodePage { | |
| 271 | const code_page = try getByIdentifier(identifier); | |
| 272 | switch (isSupported(code_page)) { | |
| 273 | true => return code_page, | |
| 274 | false => return error.UnsupportedCodePage, | |
| 275 | } | |
| 276 | } | |
| 277 | }; | |
| 278 | ||
| 279 | pub const Utf8 = struct { | |
| 280 | /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section | |
| 281 | /// D92 of Chapter 3 of the Unicode standard (Table 3-7 specifically). | |
| 282 | /// | |
| 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 | ||
| 368 | test "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 | ||
| 375 | test "codepointAt invalid utf8" { | |
| 376 | { | |
| 377 | const invalid_utf8 = "\xf0\xf0\x80\x80\x80"; | |
| 378 | try std.testing.expectEqual(Codepoint{ | |
| 379 | .value = Codepoint.invalid, | |
| 380 | .byte_len = 1, | |
| 381 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 382 | try std.testing.expectEqual(Codepoint{ | |
| 383 | .value = Codepoint.invalid, | |
| 384 | .byte_len = 2, | |
| 385 | }, CodePage.utf8.codepointAt(1, invalid_utf8).?); | |
| 386 | try std.testing.expectEqual(Codepoint{ | |
| 387 | .value = Codepoint.invalid, | |
| 388 | .byte_len = 1, | |
| 389 | }, CodePage.utf8.codepointAt(3, invalid_utf8).?); | |
| 390 | try std.testing.expectEqual(Codepoint{ | |
| 391 | .value = Codepoint.invalid, | |
| 392 | .byte_len = 1, | |
| 393 | }, CodePage.utf8.codepointAt(4, invalid_utf8).?); | |
| 394 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(5, invalid_utf8)); | |
| 395 | } | |
| 396 | ||
| 397 | { | |
| 398 | const invalid_utf8 = "\xE1\xA0\xC0"; | |
| 399 | try std.testing.expectEqual(Codepoint{ | |
| 400 | .value = Codepoint.invalid, | |
| 401 | .byte_len = 2, | |
| 402 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 403 | try std.testing.expectEqual(Codepoint{ | |
| 404 | .value = Codepoint.invalid, | |
| 405 | .byte_len = 1, | |
| 406 | }, CodePage.utf8.codepointAt(2, invalid_utf8).?); | |
| 407 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(3, invalid_utf8)); | |
| 408 | } | |
| 409 | ||
| 410 | { | |
| 411 | const invalid_utf8 = "\xD2"; | |
| 412 | try std.testing.expectEqual(Codepoint{ | |
| 413 | .value = Codepoint.invalid, | |
| 414 | .byte_len = 1, | |
| 415 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 416 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, invalid_utf8)); | |
| 417 | } | |
| 418 | ||
| 419 | { | |
| 420 | const invalid_utf8 = "\xE1\xA0"; | |
| 421 | try std.testing.expectEqual(Codepoint{ | |
| 422 | .value = Codepoint.invalid, | |
| 423 | .byte_len = 2, | |
| 424 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 425 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8)); | |
| 426 | } | |
| 427 | ||
| 428 | { | |
| 429 | const invalid_utf8 = "\xC5\xFF"; | |
| 430 | try std.testing.expectEqual(Codepoint{ | |
| 431 | .value = Codepoint.invalid, | |
| 432 | .byte_len = 1, | |
| 433 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 434 | try std.testing.expectEqual(Codepoint{ | |
| 435 | .value = Codepoint.invalid, | |
| 436 | .byte_len = 1, | |
| 437 | }, CodePage.utf8.codepointAt(1, invalid_utf8).?); | |
| 438 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8)); | |
| 439 | } | |
| 440 | ||
| 441 | { | |
| 442 | // 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 | ||
| 455 | test "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 | ||
| 477 | test "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 | ||
| 495 | pub 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 | ||
| 16 | const std = @import("std"); | |
| 17 | const Allocator = std.mem.Allocator; | |
| 18 | const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter; | |
| 19 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 20 | const LineHandler = @import("lex.zig").LineHandler; | |
| 21 | const formsLineEndingPair = @import("source_mapping.zig").formsLineEndingPair; | |
| 22 | ||
| 23 | /// `buf` must be at least as long as `source` | |
| 24 | /// In-place transformation is supported (i.e. `source` and `buf` can be the same slice) | |
| 25 | pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMappings) ![]u8 { | |
| 26 | std.debug.assert(buf.len >= source.len); | |
| 27 | var result = UncheckedSliceWriter{ .slice = buf }; | |
| 28 | const State = enum { | |
| 29 | start, | |
| 30 | forward_slash, | |
| 31 | line_comment, | |
| 32 | multiline_comment, | |
| 33 | multiline_comment_end, | |
| 34 | single_quoted, | |
| 35 | single_quoted_escape, | |
| 36 | double_quoted, | |
| 37 | double_quoted_escape, | |
| 38 | }; | |
| 39 | var state: State = .start; | |
| 40 | var index: usize = 0; | |
| 41 | var pending_start: ?usize = null; | |
| 42 | var line_handler = LineHandler{ .buffer = source }; | |
| 43 | while (index < source.len) : (index += 1) { | |
| 44 | const c = source[index]; | |
| 45 | // TODO: Disallow \x1A, \x00, \x7F in comments. At least \x1A and \x00 can definitely | |
| 46 | // cause errors or parsing weirdness in the Win32 RC compiler. These are disallowed | |
| 47 | // in the lexer, but comments are stripped before getting to the lexer. | |
| 48 | switch (state) { | |
| 49 | .start => switch (c) { | |
| 50 | '/' => { | |
| 51 | state = .forward_slash; | |
| 52 | pending_start = index; | |
| 53 | }, | |
| 54 | '\r', '\n' => { | |
| 55 | _ = line_handler.incrementLineNumber(index); | |
| 56 | result.write(c); | |
| 57 | }, | |
| 58 | else => { | |
| 59 | switch (c) { | |
| 60 | '"' => state = .double_quoted, | |
| 61 | '\'' => state = .single_quoted, | |
| 62 | else => {}, | |
| 63 | } | |
| 64 | result.write(c); | |
| 65 | }, | |
| 66 | }, | |
| 67 | .forward_slash => switch (c) { | |
| 68 | '/' => state = .line_comment, | |
| 69 | '*' => { | |
| 70 | state = .multiline_comment; | |
| 71 | }, | |
| 72 | else => { | |
| 73 | _ = line_handler.maybeIncrementLineNumber(index); | |
| 74 | result.writeSlice(source[pending_start.? .. index + 1]); | |
| 75 | pending_start = null; | |
| 76 | state = .start; | |
| 77 | }, | |
| 78 | }, | |
| 79 | .line_comment => switch (c) { | |
| 80 | '\r', '\n' => { | |
| 81 | _ = line_handler.incrementLineNumber(index); | |
| 82 | result.write(c); | |
| 83 | state = .start; | |
| 84 | }, | |
| 85 | else => {}, | |
| 86 | }, | |
| 87 | .multiline_comment => switch (c) { | |
| 88 | '\r' => 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 | ||
| 181 | inline fn handleMultilineCarriageReturn( | |
| 182 | source: []const u8, | |
| 183 | line_handler: *LineHandler, | |
| 184 | index: usize, | |
| 185 | result: *UncheckedSliceWriter, | |
| 186 | source_mappings: ?*SourceMappings, | |
| 187 | ) !void { | |
| 188 | // 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 | ||
| 216 | pub 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 | ||
| 223 | fn 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 | ||
| 230 | test "basic" { | |
| 231 | try testRemoveComments("", "// comment"); | |
| 232 | try testRemoveComments("", "/* comment */"); | |
| 233 | } | |
| 234 | ||
| 235 | test "mixed" { | |
| 236 | try testRemoveComments("hello", "hello// comment"); | |
| 237 | try testRemoveComments("hello", "hel/* comment */lo"); | |
| 238 | } | |
| 239 | ||
| 240 | test "within a string" { | |
| 241 | // escaped " is \" | |
| 242 | try testRemoveComments( | |
| 243 | \\blah"//som\"/*ething*/"BLAH | |
| 244 | , | |
| 245 | \\blah"//som\"/*ething*/"BLAH | |
| 246 | ); | |
| 247 | } | |
| 248 | ||
| 249 | test "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 | ||
| 263 | test "unfinished multiline comment" { | |
| 264 | try testRemoveComments( | |
| 265 | \\unfinished | |
| 266 | \\ | |
| 267 | , | |
| 268 | \\unfinished/* | |
| 269 | \\ | |
| 270 | ); | |
| 271 | } | |
| 272 | ||
| 273 | test "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 | ||
| 295 | test "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 | ||
| 323 | test "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 | ||
| 337 | test "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 | ||
| 354 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const Allocator = std.mem.Allocator; | |
| 4 | const Node = @import("ast.zig").Node; | |
| 5 | const lex = @import("lex.zig"); | |
| 6 | const Parser = @import("parse.zig").Parser; | |
| 7 | const Resource = @import("rc.zig").Resource; | |
| 8 | const Token = @import("lex.zig").Token; | |
| 9 | const literals = @import("literals.zig"); | |
| 10 | const Number = literals.Number; | |
| 11 | const SourceBytes = literals.SourceBytes; | |
| 12 | const Diagnostics = @import("errors.zig").Diagnostics; | |
| 13 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 14 | const MemoryFlags = @import("res.zig").MemoryFlags; | |
| 15 | const rc = @import("rc.zig"); | |
| 16 | const res = @import("res.zig"); | |
| 17 | const ico = @import("ico.zig"); | |
| 18 | const ani = @import("ani.zig"); | |
| 19 | const bmp = @import("bmp.zig"); | |
| 20 | const WORD = std.os.windows.WORD; | |
| 21 | const DWORD = std.os.windows.DWORD; | |
| 22 | const utils = @import("utils.zig"); | |
| 23 | const NameOrOrdinal = res.NameOrOrdinal; | |
| 24 | const CodePage = @import("code_pages.zig").CodePage; | |
| 25 | const CodePageLookup = @import("ast.zig").CodePageLookup; | |
| 26 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 27 | const windows1252 = @import("windows1252.zig"); | |
| 28 | const lang = @import("lang.zig"); | |
| 29 | const code_pages = @import("code_pages.zig"); | |
| 30 | const errors = @import("errors.zig"); | |
| 31 | const native_endian = builtin.cpu.arch.endian(); | |
| 32 | ||
| 33 | pub 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 | ||
| 61 | pub 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 | ||
| 169 | pub 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 | ||
| 2881 | pub const OpenSearchPathError = std.fs.Dir.OpenError; | |
| 2882 | ||
| 2883 | fn 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. | |
| 2896 | fn 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 | ||
| 2915 | pub 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` | |
| 2928 | pub 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 | ||
| 2954 | pub 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. | |
| 2963 | pub 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 | |
| 2988 | pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) { | |
| 2989 | return .{ .inner_writer = inner_writer, .bytes_left = bytes_left }; | |
| 2990 | } | |
| 2991 | ||
| 2992 | test "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 | ||
| 3006 | pub 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 | ||
| 3110 | pub 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 | ||
| 3141 | pub 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 | ||
| 3372 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Token = @import("lex.zig").Token; | |
| 3 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 4 | const utils = @import("utils.zig"); | |
| 5 | const rc = @import("rc.zig"); | |
| 6 | const res = @import("res.zig"); | |
| 7 | const ico = @import("ico.zig"); | |
| 8 | const bmp = @import("bmp.zig"); | |
| 9 | const parse = @import("parse.zig"); | |
| 10 | const lang = @import("lang.zig"); | |
| 11 | const CodePage = @import("code_pages.zig").CodePage; | |
| 12 | const builtin = @import("builtin"); | |
| 13 | const native_endian = builtin.cpu.arch.endian(); | |
| 14 | ||
| 15 | pub 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 | |
| 94 | pub const DiagnosticsContext = struct { | |
| 95 | diagnostics: *Diagnostics, | |
| 96 | token: Token, | |
| 97 | }; | |
| 98 | ||
| 99 | pub 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 | ||
| 831 | pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_config: std.io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void { | |
| 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 | ||
| 968 | const 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 | ||
| 1022 | fn writeSourceSlice(writer: anytype, slice: []const u8) !void { | |
| 1023 | for (slice) |c| try writeSourceByte(writer, c); | |
| 1024 | } | |
| 1025 | ||
| 1026 | inline 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 | ||
| 1042 | pub 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 | ||
| 1071 | pub 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 | ||
| 7 | const std = @import("std"); | |
| 8 | const builtin = @import("builtin"); | |
| 9 | const native_endian = builtin.cpu.arch.endian(); | |
| 10 | ||
| 11 | pub const ReadError = std.mem.Allocator.Error || error{ InvalidHeader, InvalidImageType, ImpossibleDataSize, UnexpectedEOF, ReadError }; | |
| 12 | ||
| 13 | pub 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. | |
| 41 | pub 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 | ||
| 113 | pub const ImageType = enum(u16) { | |
| 114 | icon = 1, | |
| 115 | cursor = 2, | |
| 116 | }; | |
| 117 | ||
| 118 | pub 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 | ||
| 151 | pub 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 | ||
| 198 | test "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 | ||
| 208 | test "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 | ||
| 217 | test "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 | ||
| 223 | test "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 | ||
| 229 | test "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 | ||
| 235 | pub 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). | |
| 263 | pub 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 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | /// This function is specific to how the Win32 RC command line interprets | |
| 4 | /// language IDs specified as integers. | |
| 5 | /// - Always interpreted as hexadecimal, but explicit 0x prefix is also allowed | |
| 6 | /// - Wraps on overflow of u16 | |
| 7 | /// - Stops parsing on any invalid hexadecimal digits | |
| 8 | /// - Errors if a digit is not the first char | |
| 9 | /// - `-` (negative) prefix is allowed | |
| 10 | pub fn parseInt(str: []const u8) error{InvalidLanguageId}!u16 { | |
| 11 | var result: u16 = 0; | |
| 12 | const radix: u8 = 16; | |
| 13 | var buf = str; | |
| 14 | ||
| 15 | const Prefix = enum { none, minus }; | |
| 16 | var prefix: Prefix = .none; | |
| 17 | switch (buf[0]) { | |
| 18 | '-' => { | |
| 19 | prefix = .minus; | |
| 20 | buf = buf[1..]; | |
| 21 | }, | |
| 22 | else => {}, | |
| 23 | } | |
| 24 | ||
| 25 | if (buf.len > 2 and buf[0] == '0' and buf[1] == 'x') { | |
| 26 | buf = buf[2..]; | |
| 27 | } | |
| 28 | ||
| 29 | for (buf, 0..) |c, i| { | |
| 30 | const digit = switch (c) { | |
| 31 | // On invalid digit for the radix, just stop parsing but don't fail | |
| 32 | 'a'...'f', 'A'...'F', '0'...'9' => std.fmt.charToDigit(c, radix) catch break, | |
| 33 | else => { | |
| 34 | // First digit must be valid | |
| 35 | if (i == 0) { | |
| 36 | return error.InvalidLanguageId; | |
| 37 | } | |
| 38 | break; | |
| 39 | }, | |
| 40 | }; | |
| 41 | ||
| 42 | if (result != 0) { | |
| 43 | result *%= radix; | |
| 44 | } | |
| 45 | result +%= digit; | |
| 46 | } | |
| 47 | ||
| 48 | switch (prefix) { | |
| 49 | .none => {}, | |
| 50 | .minus => result = 0 -% result, | |
| 51 | } | |
| 52 | ||
| 53 | return result; | |
| 54 | } | |
| 55 | ||
| 56 | test parseInt { | |
| 57 | try std.testing.expectEqual(@as(u16, 0x16), try parseInt("16")); | |
| 58 | try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1A")); | |
| 59 | try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1Azzzz")); | |
| 60 | try std.testing.expectEqual(@as(u16, 0xffff), try parseInt("-1")); | |
| 61 | try std.testing.expectEqual(@as(u16, 0xffea), try parseInt("-0x16")); | |
| 62 | try std.testing.expectEqual(@as(u16, 0x0), try parseInt("0o100")); | |
| 63 | try std.testing.expectEqual(@as(u16, 0x1), try parseInt("10001")); | |
| 64 | try std.testing.expectError(error.InvalidLanguageId, parseInt("--1")); | |
| 65 | try std.testing.expectError(error.InvalidLanguageId, parseInt("0xha")); | |
| 66 | try std.testing.expectError(error.InvalidLanguageId, parseInt("¹")); | |
| 67 | try std.testing.expectError(error.InvalidLanguageId, parseInt("~1")); | |
| 68 | } | |
| 69 | ||
| 70 | /// This function is specific to how the Win32 RC command line interprets | |
| 71 | /// language tags: invalid tags are rejected, but tags that don't have | |
| 72 | /// a specific assigned ID but are otherwise valid enough will get | |
| 73 | /// converted to an ID of LOCALE_CUSTOM_UNSPECIFIED. | |
| 74 | pub fn tagToInt(tag: []const u8) error{InvalidLanguageTag}!u16 { | |
| 75 | const maybe_id = try tagToId(tag); | |
| 76 | if (maybe_id) |id| { | |
| 77 | return @intFromEnum(id); | |
| 78 | } else { | |
| 79 | return LOCALE_CUSTOM_UNSPECIFIED; | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | pub fn tagToId(tag: []const u8) error{InvalidLanguageTag}!?LanguageId { | |
| 84 | const parsed = try parse(tag); | |
| 85 | // There are currently no language tags with assigned IDs that have | |
| 86 | // multiple suffixes, so we can skip the lookup. | |
| 87 | if (parsed.multiple_suffixes) return null; | |
| 88 | const longest_known_tag = comptime blk: { | |
| 89 | var len = 0; | |
| 90 | for (@typeInfo(LanguageId).Enum.fields) |field| { | |
| 91 | if (field.name.len > len) len = field.name.len; | |
| 92 | } | |
| 93 | break :blk len; | |
| 94 | }; | |
| 95 | // If the tag is longer than the longest tag that has an assigned ID, | |
| 96 | // then we can skip the lookup. | |
| 97 | if (tag.len > longest_known_tag) return null; | |
| 98 | var normalized_buf: [longest_known_tag]u8 = undefined; | |
| 99 | // To allow e.g. `de-de_phoneb` to get looked up as `de-de`, we need to | |
| 100 | // omit the suffix, but only if the tag contains a valid alternate sort order. | |
| 101 | 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 | ||
| 113 | test tagToId { | |
| 114 | try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("ar-ae")).?); | |
| 115 | try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("AR_AE")).?); | |
| 116 | try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-ng")).?); | |
| 117 | // Special case | |
| 118 | try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-Latn-NG")).?); | |
| 119 | } | |
| 120 | ||
| 121 | test "exhaustive tagToId" { | |
| 122 | inline for (@typeInfo(LanguageId).Enum.fields) |field| { | |
| 123 | const id = tagToId(field.name) catch |err| { | |
| 124 | std.debug.print("tag: {s}\n", .{field.name}); | |
| 125 | return err; | |
| 126 | }; | |
| 127 | try std.testing.expectEqual(@field(LanguageId, field.name), id orelse { | |
| 128 | std.debug.print("tag: {s}, got null\n", .{field.name}); | |
| 129 | return error.TestExpectedEqual; | |
| 130 | }); | |
| 131 | } | |
| 132 | var buf: [32]u8 = undefined; | |
| 133 | inline for (valid_alternate_sorts) |parsed_sort| { | |
| 134 | var fbs = std.io.fixedBufferStream(&buf); | |
| 135 | const writer = fbs.writer(); | |
| 136 | writer.writeAll(parsed_sort.language_code) catch unreachable; | |
| 137 | writer.writeAll("-") catch unreachable; | |
| 138 | writer.writeAll(parsed_sort.country_code.?) catch unreachable; | |
| 139 | writer.writeAll("-") catch unreachable; | |
| 140 | writer.writeAll(parsed_sort.suffix.?) catch unreachable; | |
| 141 | const expected_field_name = comptime field: { | |
| 142 | var name_buf: [5]u8 = undefined; | |
| 143 | @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 | ||
| 160 | fn normalizeTag(tag: []const u8, buf: []u8) []u8 { | |
| 161 | std.debug.assert(buf.len >= tag.len); | |
| 162 | for (tag, 0..) |c, i| { | |
| 163 | if (c == '-') | |
| 164 | buf[i] = '_' | |
| 165 | else | |
| 166 | buf[i] = std.ascii.toLower(c); | |
| 167 | } | |
| 168 | return buf[0..tag.len]; | |
| 169 | } | |
| 170 | ||
| 171 | /// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-LCID/%5bMS-LCID%5d.pdf#%5B%7B%22num%22%3A72%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C69%2C574%2C0%5D | |
| 172 | /// "When an LCID is requested for a locale without a | |
| 173 | /// permanent LCID assignment, nor a temporary | |
| 174 | /// assignment as above, the protocol will respond | |
| 175 | /// with LOCALE_CUSTOM_UNSPECIFIED for all such | |
| 176 | /// locales. Because this single value is used for | |
| 177 | /// numerous possible locale names, it is impossible to | |
| 178 | /// round trip this locale, even temporarily. | |
| 179 | /// Applications should discard this value as soon as | |
| 180 | /// possible and never persist it. If the system is | |
| 181 | /// forced to respond to a request for | |
| 182 | /// LCID_CUSTOM_UNSPECIFIED, it will fall back to | |
| 183 | /// the current user locale. This is often incorrect but | |
| 184 | /// may prevent an application or component from | |
| 185 | /// failing. As the meaning of this temporary LCID is | |
| 186 | /// unstable, it should never be used for interchange | |
| 187 | /// or persisted data. This is a 1-to-many relationship | |
| 188 | /// that is very unstable." | |
| 189 | pub const LOCALE_CUSTOM_UNSPECIFIED = 0x1000; | |
| 190 | ||
| 191 | pub const LANG_ENGLISH = 0x09; | |
| 192 | pub const SUBLANG_ENGLISH_US = 0x01; | |
| 193 | ||
| 194 | /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers | |
| 195 | pub fn MAKELANGID(primary: u10, sublang: u6) u16 { | |
| 196 | return (@as(u16, primary) << 10) | sublang; | |
| 197 | } | |
| 198 | ||
| 199 | /// Language tag format expressed as a regular expression (rough approximation): | |
| 200 | /// | |
| 201 | /// [a-zA-Z]{1,3}([-_][a-zA-Z]{4})?([-_][a-zA-Z]{2})?([-_][a-zA-Z0-9]{1,8})? | |
| 202 | /// lang | script | country | suffix | |
| 203 | /// | |
| 204 | /// Notes: | |
| 205 | /// - If lang code is 1 char, it seems to mean that everything afterwards uses suffix | |
| 206 | /// parsing rules (e.g. `a-0` and `a-00000000` are allowed). | |
| 207 | /// - There can also be any number of trailing suffix parts as long as they each | |
| 208 | /// would be a valid suffix part, e.g. `en-us-blah-blah1-blah2-blah3` is allowed. | |
| 209 | /// - When doing lookups, trailing suffix parts are taken into account, e.g. | |
| 210 | /// `ca-es-valencia` is not considered equivalent to `ca-es-valencia-blah`. | |
| 211 | /// - A suffix is only allowed if: | |
| 212 | /// + Lang code is 1 char long, or | |
| 213 | /// + A country code is present, or | |
| 214 | /// + A script tag is not present and: | |
| 215 | /// - the suffix is numeric-only and has a length of 3, or | |
| 216 | /// - the lang is `qps` and the suffix is `ploca` or `plocm` | |
| 217 | pub fn parse(lang_tag: []const u8) error{InvalidLanguageTag}!Parsed { | |
| 218 | var it = std.mem.splitAny(u8, lang_tag, "-_"); | |
| 219 | const lang_code = it.first(); | |
| 220 | const is_valid_lang_code = lang_code.len >= 1 and lang_code.len <= 3 and isAllAlphabetic(lang_code); | |
| 221 | if (!is_valid_lang_code) return error.InvalidLanguageTag; | |
| 222 | var parsed = Parsed{ | |
| 223 | .language_code = lang_code, | |
| 224 | }; | |
| 225 | // The second part could be a script tag, a country code, or a suffix | |
| 226 | if (it.next()) |part_str| { | |
| 227 | // The lang code being length 1 behaves strangely, so fully special case it. | |
| 228 | if (lang_code.len == 1) { | |
| 229 | // This is almost certainly not the 'right' way to do this, but I don't have a method | |
| 230 | // to determine how exactly these language tags are parsed, and it seems like | |
| 231 | // suffix parsing rules apply generally (digits allowed, length of 1 to 8). | |
| 232 | // | |
| 233 | // However, because we want to be able to lookup `x-iv-mathan` normally without | |
| 234 | // `multiple_suffixes` being set to true, we need to make sure to treat two-length | |
| 235 | // alphabetic parts as a country code. | |
| 236 | if (part_str.len == 2 and isAllAlphabetic(part_str)) { | |
| 237 | parsed.country_code = part_str; | |
| 238 | } | |
| 239 | // Everything else, though, we can just throw into the suffix as long as the normal | |
| 240 | // rules apply. | |
| 241 | else if (part_str.len > 0 and part_str.len <= 8 and isAllAlphanumeric(part_str)) { | |
| 242 | parsed.suffix = part_str; | |
| 243 | } else { | |
| 244 | return error.InvalidLanguageTag; | |
| 245 | } | |
| 246 | } else if (part_str.len == 4 and isAllAlphabetic(part_str)) { | |
| 247 | parsed.script_tag = part_str; | |
| 248 | } else if (part_str.len == 2 and isAllAlphabetic(part_str)) { | |
| 249 | parsed.country_code = part_str; | |
| 250 | } | |
| 251 | // Only a 3-len numeric suffix is allowed as the second part of a tag | |
| 252 | else if (part_str.len == 3 and isAllNumeric(part_str)) { | |
| 253 | parsed.suffix = part_str; | |
| 254 | } | |
| 255 | // Special case for qps-ploca and qps-plocm | |
| 256 | else if (std.ascii.eqlIgnoreCase(lang_code, "qps") and | |
| 257 | (std.ascii.eqlIgnoreCase(part_str, "ploca") or | |
| 258 | std.ascii.eqlIgnoreCase(part_str, "plocm"))) | |
| 259 | { | |
| 260 | parsed.suffix = part_str; | |
| 261 | } else { | |
| 262 | return error.InvalidLanguageTag; | |
| 263 | } | |
| 264 | } else { | |
| 265 | // If there's no part besides a 1-len lang code, then it is malformed | |
| 266 | if (lang_code.len == 1) return error.InvalidLanguageTag; | |
| 267 | return parsed; | |
| 268 | } | |
| 269 | if (parsed.script_tag != null) { | |
| 270 | if (it.next()) |part_str| { | |
| 271 | if (part_str.len == 2 and isAllAlphabetic(part_str)) { | |
| 272 | parsed.country_code = part_str; | |
| 273 | } else { | |
| 274 | // Suffix is not allowed when a country code is not present. | |
| 275 | return error.InvalidLanguageTag; | |
| 276 | } | |
| 277 | } else { | |
| 278 | return parsed; | |
| 279 | } | |
| 280 | } | |
| 281 | // We've now parsed any potential script tag/country codes, so anything remaining | |
| 282 | // is a suffix | |
| 283 | while (it.next()) |part_str| { | |
| 284 | if (part_str.len == 0 or part_str.len > 8 or !isAllAlphanumeric(part_str)) { | |
| 285 | return error.InvalidLanguageTag; | |
| 286 | } | |
| 287 | if (parsed.suffix == null) { | |
| 288 | parsed.suffix = part_str; | |
| 289 | } else { | |
| 290 | // In theory we could return early here but we still want to validate | |
| 291 | // that each part is a valid suffix all the way to the end, e.g. | |
| 292 | // we should reject `en-us-suffix-a-b-c-!!!` because of the invalid `!!!` | |
| 293 | // suffix part. | |
| 294 | parsed.multiple_suffixes = true; | |
| 295 | } | |
| 296 | } | |
| 297 | return parsed; | |
| 298 | } | |
| 299 | ||
| 300 | pub const Parsed = struct { | |
| 301 | language_code: []const u8, | |
| 302 | script_tag: ?[]const u8 = null, | |
| 303 | country_code: ?[]const u8 = null, | |
| 304 | /// Can be a sort order (e.g. phoneb) or something like valencia, 001, etc | |
| 305 | suffix: ?[]const u8 = null, | |
| 306 | /// There can be any number of suffixes, but we don't need to care what their | |
| 307 | /// values are, we just need to know if any exist so that e.g. `ca-es-valencia-blah` | |
| 308 | /// can be seen as different from `ca-es-valencia`. Storing this as a bool | |
| 309 | /// allows us to avoid needing either (a) dynamic allocation or (b) a limit to | |
| 310 | /// the number of suffixes allowed when parsing. | |
| 311 | multiple_suffixes: bool = false, | |
| 312 | ||
| 313 | pub fn isSuffixValidSortOrder(self: Parsed) bool { | |
| 314 | if (self.country_code == null) return false; | |
| 315 | if (self.suffix == null) return false; | |
| 316 | if (self.script_tag != null) return false; | |
| 317 | if (self.multiple_suffixes) return false; | |
| 318 | for (valid_alternate_sorts) |valid_sort| { | |
| 319 | if (std.ascii.eqlIgnoreCase(valid_sort.language_code, self.language_code) and | |
| 320 | std.ascii.eqlIgnoreCase(valid_sort.country_code.?, self.country_code.?) and | |
| 321 | std.ascii.eqlIgnoreCase(valid_sort.suffix.?, self.suffix.?)) | |
| 322 | { | |
| 323 | return true; | |
| 324 | } | |
| 325 | } | |
| 326 | return false; | |
| 327 | } | |
| 328 | }; | |
| 329 | ||
| 330 | /// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f | |
| 331 | /// See the table following this text: "Alternate sorts can be selected by using one of the identifiers from the following table." | |
| 332 | const valid_alternate_sorts = [_]Parsed{ | |
| 333 | // Note: x-IV-mathan is omitted due to how lookups are implemented. | |
| 334 | // This table is used to make e.g. `de-de_phoneb` get looked up | |
| 335 | // as `de-de` (the suffix is omitted for the lookup), but x-iv-mathan | |
| 336 | // instead needs to be looked up with the suffix included because | |
| 337 | // `x-iv` is not a tag with an assigned ID. | |
| 338 | .{ .language_code = "de", .country_code = "de", .suffix = "phoneb" }, | |
| 339 | .{ .language_code = "hu", .country_code = "hu", .suffix = "tchncl" }, | |
| 340 | .{ .language_code = "ka", .country_code = "ge", .suffix = "modern" }, | |
| 341 | .{ .language_code = "zh", .country_code = "cn", .suffix = "stroke" }, | |
| 342 | .{ .language_code = "zh", .country_code = "sg", .suffix = "stroke" }, | |
| 343 | .{ .language_code = "zh", .country_code = "mo", .suffix = "stroke" }, | |
| 344 | .{ .language_code = "zh", .country_code = "tw", .suffix = "pronun" }, | |
| 345 | .{ .language_code = "zh", .country_code = "tw", .suffix = "radstr" }, | |
| 346 | .{ .language_code = "ja", .country_code = "jp", .suffix = "radstr" }, | |
| 347 | .{ .language_code = "zh", .country_code = "hk", .suffix = "radstr" }, | |
| 348 | .{ .language_code = "zh", .country_code = "mo", .suffix = "radstr" }, | |
| 349 | .{ .language_code = "zh", .country_code = "cn", .suffix = "phoneb" }, | |
| 350 | .{ .language_code = "zh", .country_code = "sg", .suffix = "phoneb" }, | |
| 351 | }; | |
| 352 | ||
| 353 | test "parse" { | |
| 354 | try std.testing.expectEqualDeep(Parsed{ | |
| 355 | .language_code = "en", | |
| 356 | }, try parse("en")); | |
| 357 | try std.testing.expectEqualDeep(Parsed{ | |
| 358 | .language_code = "en", | |
| 359 | .country_code = "us", | |
| 360 | }, try parse("en-us")); | |
| 361 | try std.testing.expectEqualDeep(Parsed{ | |
| 362 | .language_code = "en", | |
| 363 | .suffix = "123", | |
| 364 | }, try parse("en-123")); | |
| 365 | try std.testing.expectEqualDeep(Parsed{ | |
| 366 | .language_code = "en", | |
| 367 | .suffix = "123", | |
| 368 | .multiple_suffixes = true, | |
| 369 | }, try parse("en-123-blah")); | |
| 370 | try std.testing.expectEqualDeep(Parsed{ | |
| 371 | .language_code = "en", | |
| 372 | .country_code = "us", | |
| 373 | .suffix = "123", | |
| 374 | .multiple_suffixes = true, | |
| 375 | }, try parse("en-us_123-blah")); | |
| 376 | try std.testing.expectEqualDeep(Parsed{ | |
| 377 | .language_code = "eng", | |
| 378 | .script_tag = "Latn", | |
| 379 | }, try parse("eng-Latn")); | |
| 380 | try std.testing.expectEqualDeep(Parsed{ | |
| 381 | .language_code = "eng", | |
| 382 | .script_tag = "Latn", | |
| 383 | }, try parse("eng-Latn")); | |
| 384 | try std.testing.expectEqualDeep(Parsed{ | |
| 385 | .language_code = "ff", | |
| 386 | .script_tag = "Latn", | |
| 387 | .country_code = "NG", | |
| 388 | }, try parse("ff-Latn-NG")); | |
| 389 | try std.testing.expectEqualDeep(Parsed{ | |
| 390 | .language_code = "qps", | |
| 391 | .suffix = "Plocm", | |
| 392 | }, try parse("qps-Plocm")); | |
| 393 | try std.testing.expectEqualDeep(Parsed{ | |
| 394 | .language_code = "qps", | |
| 395 | .suffix = "ploca", | |
| 396 | }, try parse("qps-ploca")); | |
| 397 | try std.testing.expectEqualDeep(Parsed{ | |
| 398 | .language_code = "x", | |
| 399 | .country_code = "IV", | |
| 400 | .suffix = "mathan", | |
| 401 | }, try parse("x-IV-mathan")); | |
| 402 | try std.testing.expectEqualDeep(Parsed{ | |
| 403 | .language_code = "a", | |
| 404 | .suffix = "a", | |
| 405 | }, try parse("a-a")); | |
| 406 | try std.testing.expectEqualDeep(Parsed{ | |
| 407 | .language_code = "a", | |
| 408 | .suffix = "000", | |
| 409 | }, try parse("a-000")); | |
| 410 | try std.testing.expectEqualDeep(Parsed{ | |
| 411 | .language_code = "a", | |
| 412 | .suffix = "00000000", | |
| 413 | }, try parse("a-00000000")); | |
| 414 | // suffix not allowed if script tag is present without country code | |
| 415 | try std.testing.expectError(error.InvalidLanguageTag, parse("eng-Latn-suffix")); | |
| 416 | // suffix must be 3 numeric digits if neither script tag nor country code is present | |
| 417 | try std.testing.expectError(error.InvalidLanguageTag, parse("eng-suffix")); | |
| 418 | try std.testing.expectError(error.InvalidLanguageTag, parse("en-plocm")); | |
| 419 | // 1-len lang code is not allowed if it's the only part | |
| 420 | try std.testing.expectError(error.InvalidLanguageTag, parse("e")); | |
| 421 | } | |
| 422 | ||
| 423 | fn isAllAlphabetic(str: []const u8) bool { | |
| 424 | for (str) |c| { | |
| 425 | if (!std.ascii.isAlphabetic(c)) return false; | |
| 426 | } | |
| 427 | return true; | |
| 428 | } | |
| 429 | ||
| 430 | fn isAllAlphanumeric(str: []const u8) bool { | |
| 431 | for (str) |c| { | |
| 432 | if (!std.ascii.isAlphanumeric(c)) return false; | |
| 433 | } | |
| 434 | return true; | |
| 435 | } | |
| 436 | ||
| 437 | fn isAllNumeric(str: []const u8) bool { | |
| 438 | for (str) |c| { | |
| 439 | if (!std.ascii.isDigit(c)) return false; | |
| 440 | } | |
| 441 | return true; | |
| 442 | } | |
| 443 | ||
| 444 | /// Derived from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f | |
| 445 | /// - Protocol Revision: 15.0 | |
| 446 | /// - Language / Language ID / Language Tag table in Appendix A | |
| 447 | /// - Removed all rows that have Language ID 0x1000 (LOCALE_CUSTOM_UNSPECIFIED) | |
| 448 | /// - Normalized each language tag (lowercased, replaced all `-` with `_`) | |
| 449 | /// - There is one special case where two tags are mapped to the same ID, the following | |
| 450 | /// has been omitted and must be special cased during lookup to map to the ID ff_ng / 0x0467. | |
| 451 | /// ff_latn_ng = 0x0467, // Fulah (Latin), Nigeria | |
| 452 | /// - x_iv_mathan has been added which is not in the table but does appear in the Alternate sorts | |
| 453 | /// table as 0x007F (LANG_INVARIANT). | |
| 454 | pub const LanguageId = enum(u16) { | |
| 455 | // Language tag = Language ID, // Language, Location (or type) | |
| 456 | af = 0x0036, // Afrikaans | |
| 457 | af_za = 0x0436, // Afrikaans, South Africa | |
| 458 | sq = 0x001C, // Albanian | |
| 459 | sq_al = 0x041C, // Albanian, Albania | |
| 460 | gsw = 0x0084, // Alsatian | |
| 461 | gsw_fr = 0x0484, // Alsatian, France | |
| 462 | am = 0x005E, // Amharic | |
| 463 | am_et = 0x045E, // Amharic, Ethiopia | |
| 464 | ar = 0x0001, // Arabic | |
| 465 | ar_dz = 0x1401, // Arabic, Algeria | |
| 466 | ar_bh = 0x3C01, // Arabic, Bahrain | |
| 467 | ar_eg = 0x0c01, // Arabic, Egypt | |
| 468 | ar_iq = 0x0801, // Arabic, Iraq | |
| 469 | ar_jo = 0x2C01, // Arabic, Jordan | |
| 470 | ar_kw = 0x3401, // Arabic, Kuwait | |
| 471 | ar_lb = 0x3001, // Arabic, Lebanon | |
| 472 | ar_ly = 0x1001, // Arabic, Libya | |
| 473 | ar_ma = 0x1801, // Arabic, Morocco | |
| 474 | ar_om = 0x2001, // Arabic, Oman | |
| 475 | ar_qa = 0x4001, // Arabic, Qatar | |
| 476 | ar_sa = 0x0401, // Arabic, Saudi Arabia | |
| 477 | ar_sy = 0x2801, // Arabic, Syria | |
| 478 | ar_tn = 0x1C01, // Arabic, Tunisia | |
| 479 | ar_ae = 0x3801, // Arabic, U.A.E. | |
| 480 | ar_ye = 0x2401, // Arabic, Yemen | |
| 481 | hy = 0x002B, // Armenian | |
| 482 | hy_am = 0x042B, // Armenian, Armenia | |
| 483 | as = 0x004D, // Assamese | |
| 484 | as_in = 0x044D, // Assamese, India | |
| 485 | az_cyrl = 0x742C, // Azerbaijani (Cyrillic) | |
| 486 | az_cyrl_az = 0x082C, // Azerbaijani (Cyrillic), Azerbaijan | |
| 487 | az = 0x002C, // Azerbaijani (Latin) | |
| 488 | az_latn = 0x782C, // Azerbaijani (Latin) | |
| 489 | az_latn_az = 0x042C, // Azerbaijani (Latin), Azerbaijan | |
| 490 | bn = 0x0045, // Bangla | |
| 491 | bn_bd = 0x0845, // Bangla, Bangladesh | |
| 492 | bn_in = 0x0445, // Bangla, India | |
| 493 | ba = 0x006D, // Bashkir | |
| 494 | ba_ru = 0x046D, // Bashkir, Russia | |
| 495 | eu = 0x002D, // Basque | |
| 496 | eu_es = 0x042D, // Basque, Spain | |
| 497 | be = 0x0023, // Belarusian | |
| 498 | be_by = 0x0423, // Belarusian, Belarus | |
| 499 | bs_cyrl = 0x641A, // Bosnian (Cyrillic) | |
| 500 | bs_cyrl_ba = 0x201A, // Bosnian (Cyrillic), Bosnia and Herzegovina | |
| 501 | bs_latn = 0x681A, // Bosnian (Latin) | |
| 502 | bs = 0x781A, // Bosnian (Latin) | |
| 503 | bs_latn_ba = 0x141A, // Bosnian (Latin), Bosnia and Herzegovina | |
| 504 | br = 0x007E, // Breton | |
| 505 | br_fr = 0x047E, // Breton, France | |
| 506 | bg = 0x0002, // Bulgarian | |
| 507 | bg_bg = 0x0402, // Bulgarian, Bulgaria | |
| 508 | my = 0x0055, // Burmese | |
| 509 | my_mm = 0x0455, // Burmese, Myanmar | |
| 510 | ca = 0x0003, // Catalan | |
| 511 | ca_es = 0x0403, // Catalan, Spain | |
| 512 | tzm_arab_ma = 0x045F, // Central Atlas Tamazight (Arabic), Morocco | |
| 513 | ku = 0x0092, // Central Kurdish | |
| 514 | ku_arab = 0x7c92, // Central Kurdish | |
| 515 | ku_arab_iq = 0x0492, // Central Kurdish, Iraq | |
| 516 | chr = 0x005C, // Cherokee | |
| 517 | chr_cher = 0x7c5C, // Cherokee | |
| 518 | chr_cher_us = 0x045C, // Cherokee, United States | |
| 519 | zh_hans = 0x0004, // Chinese (Simplified) | |
| 520 | zh = 0x7804, // Chinese (Simplified) | |
| 521 | zh_cn = 0x0804, // Chinese (Simplified), People's Republic of China | |
| 522 | zh_sg = 0x1004, // Chinese (Simplified), Singapore | |
| 523 | zh_hant = 0x7C04, // Chinese (Traditional) | |
| 524 | zh_hk = 0x0C04, // Chinese (Traditional), Hong Kong S.A.R. | |
| 525 | zh_mo = 0x1404, // Chinese (Traditional), Macao S.A.R. | |
| 526 | zh_tw = 0x0404, // Chinese (Traditional), Taiwan | |
| 527 | co = 0x0083, // Corsican | |
| 528 | co_fr = 0x0483, // Corsican, France | |
| 529 | hr = 0x001A, // Croatian | |
| 530 | hr_hr = 0x041A, // Croatian, Croatia | |
| 531 | hr_ba = 0x101A, // Croatian (Latin), Bosnia and Herzegovina | |
| 532 | cs = 0x0005, // Czech | |
| 533 | cs_cz = 0x0405, // Czech, Czech Republic | |
| 534 | da = 0x0006, // Danish | |
| 535 | da_dk = 0x0406, // Danish, Denmark | |
| 536 | prs = 0x008C, // Dari | |
| 537 | prs_af = 0x048C, // Dari, Afghanistan | |
| 538 | dv = 0x0065, // Divehi | |
| 539 | dv_mv = 0x0465, // Divehi, Maldives | |
| 540 | nl = 0x0013, // Dutch | |
| 541 | nl_be = 0x0813, // Dutch, Belgium | |
| 542 | nl_nl = 0x0413, // Dutch, Netherlands | |
| 543 | dz_bt = 0x0C51, // Dzongkha, Bhutan | |
| 544 | en = 0x0009, // English | |
| 545 | en_au = 0x0C09, // English, Australia | |
| 546 | en_bz = 0x2809, // English, Belize | |
| 547 | en_ca = 0x1009, // English, Canada | |
| 548 | en_029 = 0x2409, // English, Caribbean | |
| 549 | en_hk = 0x3C09, // English, Hong Kong | |
| 550 | en_in = 0x4009, // English, India | |
| 551 | en_ie = 0x1809, // English, Ireland | |
| 552 | en_jm = 0x2009, // English, Jamaica | |
| 553 | en_my = 0x4409, // English, Malaysia | |
| 554 | en_nz = 0x1409, // English, New Zealand | |
| 555 | en_ph = 0x3409, // English, Republic of the Philippines | |
| 556 | en_sg = 0x4809, // English, Singapore | |
| 557 | en_za = 0x1C09, // English, South Africa | |
| 558 | en_tt = 0x2c09, // English, Trinidad and Tobago | |
| 559 | en_ae = 0x4C09, // English, United Arab Emirates | |
| 560 | en_gb = 0x0809, // English, United Kingdom | |
| 561 | en_us = 0x0409, // English, United States | |
| 562 | en_zw = 0x3009, // English, Zimbabwe | |
| 563 | et = 0x0025, // Estonian | |
| 564 | et_ee = 0x0425, // Estonian, Estonia | |
| 565 | fo = 0x0038, // Faroese | |
| 566 | fo_fo = 0x0438, // Faroese, Faroe Islands | |
| 567 | fil = 0x0064, // Filipino | |
| 568 | fil_ph = 0x0464, // Filipino, Philippines | |
| 569 | fi = 0x000B, // Finnish | |
| 570 | fi_fi = 0x040B, // Finnish, Finland | |
| 571 | fr = 0x000C, // French | |
| 572 | fr_be = 0x080C, // French, Belgium | |
| 573 | fr_cm = 0x2c0C, // French, Cameroon | |
| 574 | fr_ca = 0x0c0C, // French, Canada | |
| 575 | fr_029 = 0x1C0C, // French, Caribbean | |
| 576 | fr_cd = 0x240C, // French, Congo, DRC | |
| 577 | fr_ci = 0x300C, // French, Côte d'Ivoire | |
| 578 | fr_fr = 0x040C, // French, France | |
| 579 | fr_ht = 0x3c0C, // French, Haiti | |
| 580 | fr_lu = 0x140C, // French, Luxembourg | |
| 581 | fr_ml = 0x340C, // French, Mali | |
| 582 | fr_ma = 0x380C, // French, Morocco | |
| 583 | fr_mc = 0x180C, // French, Principality of Monaco | |
| 584 | fr_re = 0x200C, // French, Reunion | |
| 585 | fr_sn = 0x280C, // French, Senegal | |
| 586 | fr_ch = 0x100C, // French, Switzerland | |
| 587 | fy = 0x0062, // Frisian | |
| 588 | fy_nl = 0x0462, // Frisian, Netherlands | |
| 589 | ff = 0x0067, // Fulah | |
| 590 | ff_latn = 0x7C67, // Fulah (Latin) | |
| 591 | ff_ng = 0x0467, // Fulah, Nigeria | |
| 592 | ff_latn_sn = 0x0867, // Fulah, Senegal | |
| 593 | gl = 0x0056, // Galician | |
| 594 | gl_es = 0x0456, // Galician, Spain | |
| 595 | ka = 0x0037, // Georgian | |
| 596 | ka_ge = 0x0437, // Georgian, Georgia | |
| 597 | de = 0x0007, // German | |
| 598 | de_at = 0x0C07, // German, Austria | |
| 599 | de_de = 0x0407, // German, Germany | |
| 600 | de_li = 0x1407, // German, Liechtenstein | |
| 601 | de_lu = 0x1007, // German, Luxembourg | |
| 602 | de_ch = 0x0807, // German, Switzerland | |
| 603 | el = 0x0008, // Greek | |
| 604 | el_gr = 0x0408, // Greek, Greece | |
| 605 | kl = 0x006F, // Greenlandic | |
| 606 | kl_gl = 0x046F, // Greenlandic, Greenland | |
| 607 | gn = 0x0074, // Guarani | |
| 608 | gn_py = 0x0474, // Guarani, Paraguay | |
| 609 | gu = 0x0047, // Gujarati | |
| 610 | gu_in = 0x0447, // Gujarati, India | |
| 611 | ha = 0x0068, // Hausa (Latin) | |
| 612 | ha_latn = 0x7C68, // Hausa (Latin) | |
| 613 | ha_latn_ng = 0x0468, // Hausa (Latin), Nigeria | |
| 614 | haw = 0x0075, // Hawaiian | |
| 615 | haw_us = 0x0475, // Hawaiian, United States | |
| 616 | he = 0x000D, // Hebrew | |
| 617 | he_il = 0x040D, // Hebrew, Israel | |
| 618 | hi = 0x0039, // Hindi | |
| 619 | hi_in = 0x0439, // Hindi, India | |
| 620 | hu = 0x000E, // Hungarian | |
| 621 | hu_hu = 0x040E, // Hungarian, Hungary | |
| 622 | is = 0x000F, // Icelandic | |
| 623 | is_is = 0x040F, // Icelandic, Iceland | |
| 624 | ig = 0x0070, // Igbo | |
| 625 | ig_ng = 0x0470, // Igbo, Nigeria | |
| 626 | id = 0x0021, // Indonesian | |
| 627 | id_id = 0x0421, // Indonesian, Indonesia | |
| 628 | iu = 0x005D, // Inuktitut (Latin) | |
| 629 | iu_latn = 0x7C5D, // Inuktitut (Latin) | |
| 630 | iu_latn_ca = 0x085D, // Inuktitut (Latin), Canada | |
| 631 | iu_cans = 0x785D, // Inuktitut (Syllabics) | |
| 632 | iu_cans_ca = 0x045d, // Inuktitut (Syllabics), Canada | |
| 633 | ga = 0x003C, // Irish | |
| 634 | ga_ie = 0x083C, // Irish, Ireland | |
| 635 | it = 0x0010, // Italian | |
| 636 | it_it = 0x0410, // Italian, Italy | |
| 637 | it_ch = 0x0810, // Italian, Switzerland | |
| 638 | ja = 0x0011, // Japanese | |
| 639 | ja_jp = 0x0411, // Japanese, Japan | |
| 640 | kn = 0x004B, // Kannada | |
| 641 | kn_in = 0x044B, // Kannada, India | |
| 642 | kr_latn_ng = 0x0471, // Kanuri (Latin), Nigeria | |
| 643 | ks = 0x0060, // Kashmiri | |
| 644 | ks_arab = 0x0460, // Kashmiri, Perso-Arabic | |
| 645 | ks_deva_in = 0x0860, // Kashmiri (Devanagari), India | |
| 646 | kk = 0x003F, // Kazakh | |
| 647 | kk_kz = 0x043F, // Kazakh, Kazakhstan | |
| 648 | km = 0x0053, // Khmer | |
| 649 | km_kh = 0x0453, // Khmer, Cambodia | |
| 650 | quc = 0x0086, // K'iche | |
| 651 | quc_latn_gt = 0x0486, // K'iche, Guatemala | |
| 652 | rw = 0x0087, // Kinyarwanda | |
| 653 | rw_rw = 0x0487, // Kinyarwanda, Rwanda | |
| 654 | sw = 0x0041, // Kiswahili | |
| 655 | sw_ke = 0x0441, // Kiswahili, Kenya | |
| 656 | kok = 0x0057, // Konkani | |
| 657 | kok_in = 0x0457, // Konkani, India | |
| 658 | ko = 0x0012, // Korean | |
| 659 | ko_kr = 0x0412, // Korean, Korea | |
| 660 | ky = 0x0040, // Kyrgyz | |
| 661 | ky_kg = 0x0440, // Kyrgyz, Kyrgyzstan | |
| 662 | lo = 0x0054, // Lao | |
| 663 | lo_la = 0x0454, // Lao, Lao P.D.R. | |
| 664 | la_va = 0x0476, // Latin, Vatican City | |
| 665 | lv = 0x0026, // Latvian | |
| 666 | lv_lv = 0x0426, // Latvian, Latvia | |
| 667 | lt = 0x0027, // Lithuanian | |
| 668 | lt_lt = 0x0427, // Lithuanian, Lithuania | |
| 669 | dsb = 0x7C2E, // Lower Sorbian | |
| 670 | dsb_de = 0x082E, // Lower Sorbian, Germany | |
| 671 | lb = 0x006E, // Luxembourgish | |
| 672 | lb_lu = 0x046E, // Luxembourgish, Luxembourg | |
| 673 | mk = 0x002F, // Macedonian | |
| 674 | mk_mk = 0x042F, // Macedonian, North Macedonia | |
| 675 | ms = 0x003E, // Malay | |
| 676 | ms_bn = 0x083E, // Malay, Brunei Darussalam | |
| 677 | ms_my = 0x043E, // Malay, Malaysia | |
| 678 | ml = 0x004C, // Malayalam | |
| 679 | ml_in = 0x044C, // Malayalam, India | |
| 680 | mt = 0x003A, // Maltese | |
| 681 | mt_mt = 0x043A, // Maltese, Malta | |
| 682 | mi = 0x0081, // Maori | |
| 683 | mi_nz = 0x0481, // Maori, New Zealand | |
| 684 | arn = 0x007A, // Mapudungun | |
| 685 | arn_cl = 0x047A, // Mapudungun, Chile | |
| 686 | mr = 0x004E, // Marathi | |
| 687 | mr_in = 0x044E, // Marathi, India | |
| 688 | moh = 0x007C, // Mohawk | |
| 689 | moh_ca = 0x047C, // Mohawk, Canada | |
| 690 | mn = 0x0050, // Mongolian (Cyrillic) | |
| 691 | mn_cyrl = 0x7850, // Mongolian (Cyrillic) | |
| 692 | mn_mn = 0x0450, // Mongolian (Cyrillic), Mongolia | |
| 693 | mn_mong = 0x7C50, // Mongolian (Traditional Mongolian) | |
| 694 | mn_mong_cn = 0x0850, // Mongolian (Traditional Mongolian), People's Republic of China | |
| 695 | mn_mong_mn = 0x0C50, // Mongolian (Traditional Mongolian), Mongolia | |
| 696 | ne = 0x0061, // Nepali | |
| 697 | ne_in = 0x0861, // Nepali, India | |
| 698 | ne_np = 0x0461, // Nepali, Nepal | |
| 699 | no = 0x0014, // Norwegian (Bokmal) | |
| 700 | nb = 0x7C14, // Norwegian (Bokmal) | |
| 701 | nb_no = 0x0414, // Norwegian (Bokmal), Norway | |
| 702 | nn = 0x7814, // Norwegian (Nynorsk) | |
| 703 | nn_no = 0x0814, // Norwegian (Nynorsk), Norway | |
| 704 | oc = 0x0082, // Occitan | |
| 705 | oc_fr = 0x0482, // Occitan, France | |
| 706 | @"or" = 0x0048, // Odia | |
| 707 | or_in = 0x0448, // Odia, India | |
| 708 | om = 0x0072, // Oromo | |
| 709 | om_et = 0x0472, // Oromo, Ethiopia | |
| 710 | ps = 0x0063, // Pashto | |
| 711 | ps_af = 0x0463, // Pashto, Afghanistan | |
| 712 | fa = 0x0029, // Persian | |
| 713 | fa_ir = 0x0429, // Persian, Iran | |
| 714 | pl = 0x0015, // Polish | |
| 715 | pl_pl = 0x0415, // Polish, Poland | |
| 716 | pt = 0x0016, // Portuguese | |
| 717 | pt_br = 0x0416, // Portuguese, Brazil | |
| 718 | pt_pt = 0x0816, // Portuguese, Portugal | |
| 719 | qps_ploca = 0x05FE, // Pseudo Language, Pseudo locale for east Asian/complex script localization testing | |
| 720 | qps_ploc = 0x0501, // Pseudo Language, Pseudo locale used for localization testing | |
| 721 | qps_plocm = 0x09FF, // Pseudo Language, Pseudo locale used for localization testing of mirrored locales | |
| 722 | pa = 0x0046, // Punjabi | |
| 723 | pa_arab = 0x7C46, // Punjabi | |
| 724 | pa_in = 0x0446, // Punjabi, India | |
| 725 | pa_arab_pk = 0x0846, // Punjabi, Islamic Republic of Pakistan | |
| 726 | quz = 0x006B, // Quechua | |
| 727 | quz_bo = 0x046B, // Quechua, Bolivia | |
| 728 | quz_ec = 0x086B, // Quechua, Ecuador | |
| 729 | quz_pe = 0x0C6B, // Quechua, Peru | |
| 730 | ro = 0x0018, // Romanian | |
| 731 | ro_md = 0x0818, // Romanian, Moldova | |
| 732 | ro_ro = 0x0418, // Romanian, Romania | |
| 733 | rm = 0x0017, // Romansh | |
| 734 | rm_ch = 0x0417, // Romansh, Switzerland | |
| 735 | ru = 0x0019, // Russian | |
| 736 | ru_md = 0x0819, // Russian, Moldova | |
| 737 | ru_ru = 0x0419, // Russian, Russia | |
| 738 | sah = 0x0085, // Sakha | |
| 739 | sah_ru = 0x0485, // Sakha, Russia | |
| 740 | smn = 0x703B, // Sami (Inari) | |
| 741 | smn_fi = 0x243B, // Sami (Inari), Finland | |
| 742 | smj = 0x7C3B, // Sami (Lule) | |
| 743 | smj_no = 0x103B, // Sami (Lule), Norway | |
| 744 | smj_se = 0x143B, // Sami (Lule), Sweden | |
| 745 | se = 0x003B, // Sami (Northern) | |
| 746 | se_fi = 0x0C3B, // Sami (Northern), Finland | |
| 747 | se_no = 0x043B, // Sami (Northern), Norway | |
| 748 | se_se = 0x083B, // Sami (Northern), Sweden | |
| 749 | sms = 0x743B, // Sami (Skolt) | |
| 750 | sms_fi = 0x203B, // Sami (Skolt), Finland | |
| 751 | sma = 0x783B, // Sami (Southern) | |
| 752 | sma_no = 0x183B, // Sami (Southern), Norway | |
| 753 | sma_se = 0x1C3B, // Sami (Southern), Sweden | |
| 754 | sa = 0x004F, // Sanskrit | |
| 755 | sa_in = 0x044F, // Sanskrit, India | |
| 756 | gd = 0x0091, // Scottish Gaelic | |
| 757 | gd_gb = 0x0491, // Scottish Gaelic, United Kingdom | |
| 758 | sr_cyrl = 0x6C1A, // Serbian (Cyrillic) | |
| 759 | sr_cyrl_ba = 0x1C1A, // Serbian (Cyrillic), Bosnia and Herzegovina | |
| 760 | sr_cyrl_me = 0x301A, // Serbian (Cyrillic), Montenegro | |
| 761 | sr_cyrl_rs = 0x281A, // Serbian (Cyrillic), Serbia | |
| 762 | sr_cyrl_cs = 0x0C1A, // Serbian (Cyrillic), Serbia and Montenegro (Former) | |
| 763 | sr_latn = 0x701A, // Serbian (Latin) | |
| 764 | sr = 0x7C1A, // Serbian (Latin) | |
| 765 | sr_latn_ba = 0x181A, // Serbian (Latin), Bosnia and Herzegovina | |
| 766 | sr_latn_me = 0x2c1A, // Serbian (Latin), Montenegro | |
| 767 | sr_latn_rs = 0x241A, // Serbian (Latin), Serbia | |
| 768 | sr_latn_cs = 0x081A, // Serbian (Latin), Serbia and Montenegro (Former) | |
| 769 | nso = 0x006C, // Sesotho sa Leboa | |
| 770 | nso_za = 0x046C, // Sesotho sa Leboa, South Africa | |
| 771 | tn = 0x0032, // Setswana | |
| 772 | tn_bw = 0x0832, // Setswana, Botswana | |
| 773 | tn_za = 0x0432, // Setswana, South Africa | |
| 774 | sd = 0x0059, // Sindhi | |
| 775 | sd_arab = 0x7C59, // Sindhi | |
| 776 | sd_arab_pk = 0x0859, // Sindhi, Islamic Republic of Pakistan | |
| 777 | si = 0x005B, // Sinhala | |
| 778 | si_lk = 0x045B, // Sinhala, Sri Lanka | |
| 779 | sk = 0x001B, // Slovak | |
| 780 | sk_sk = 0x041B, // Slovak, Slovakia | |
| 781 | sl = 0x0024, // Slovenian | |
| 782 | sl_si = 0x0424, // Slovenian, Slovenia | |
| 783 | so = 0x0077, // Somali | |
| 784 | so_so = 0x0477, // Somali, Somalia | |
| 785 | st = 0x0030, // Sotho | |
| 786 | st_za = 0x0430, // Sotho, South Africa | |
| 787 | es = 0x000A, // Spanish | |
| 788 | es_ar = 0x2C0A, // Spanish, Argentina | |
| 789 | es_ve = 0x200A, // Spanish, Bolivarian Republic of Venezuela | |
| 790 | es_bo = 0x400A, // Spanish, Bolivia | |
| 791 | es_cl = 0x340A, // Spanish, Chile | |
| 792 | es_co = 0x240A, // Spanish, Colombia | |
| 793 | es_cr = 0x140A, // Spanish, Costa Rica | |
| 794 | es_cu = 0x5c0A, // Spanish, Cuba | |
| 795 | es_do = 0x1c0A, // Spanish, Dominican Republic | |
| 796 | es_ec = 0x300A, // Spanish, Ecuador | |
| 797 | es_sv = 0x440A, // Spanish, El Salvador | |
| 798 | es_gt = 0x100A, // Spanish, Guatemala | |
| 799 | es_hn = 0x480A, // Spanish, Honduras | |
| 800 | es_419 = 0x580A, // Spanish, Latin America | |
| 801 | es_mx = 0x080A, // Spanish, Mexico | |
| 802 | es_ni = 0x4C0A, // Spanish, Nicaragua | |
| 803 | es_pa = 0x180A, // Spanish, Panama | |
| 804 | es_py = 0x3C0A, // Spanish, Paraguay | |
| 805 | es_pe = 0x280A, // Spanish, Peru | |
| 806 | es_pr = 0x500A, // Spanish, Puerto Rico | |
| 807 | es_es_tradnl = 0x040A, // Spanish, Spain | |
| 808 | es_es = 0x0c0A, // Spanish, Spain | |
| 809 | es_us = 0x540A, // Spanish, United States | |
| 810 | es_uy = 0x380A, // Spanish, Uruguay | |
| 811 | sv = 0x001D, // Swedish | |
| 812 | sv_fi = 0x081D, // Swedish, Finland | |
| 813 | sv_se = 0x041D, // Swedish, Sweden | |
| 814 | syr = 0x005A, // Syriac | |
| 815 | syr_sy = 0x045A, // Syriac, Syria | |
| 816 | tg = 0x0028, // Tajik (Cyrillic) | |
| 817 | tg_cyrl = 0x7C28, // Tajik (Cyrillic) | |
| 818 | tg_cyrl_tj = 0x0428, // Tajik (Cyrillic), Tajikistan | |
| 819 | tzm = 0x005F, // Tamazight (Latin) | |
| 820 | tzm_latn = 0x7C5F, // Tamazight (Latin) | |
| 821 | tzm_latn_dz = 0x085F, // Tamazight (Latin), Algeria | |
| 822 | ta = 0x0049, // Tamil | |
| 823 | ta_in = 0x0449, // Tamil, India | |
| 824 | ta_lk = 0x0849, // Tamil, Sri Lanka | |
| 825 | tt = 0x0044, // Tatar | |
| 826 | tt_ru = 0x0444, // Tatar, Russia | |
| 827 | te = 0x004A, // Telugu | |
| 828 | te_in = 0x044A, // Telugu, India | |
| 829 | th = 0x001E, // Thai | |
| 830 | th_th = 0x041E, // Thai, Thailand | |
| 831 | bo = 0x0051, // Tibetan | |
| 832 | bo_cn = 0x0451, // Tibetan, People's Republic of China | |
| 833 | ti = 0x0073, // Tigrinya | |
| 834 | ti_er = 0x0873, // Tigrinya, Eritrea | |
| 835 | ti_et = 0x0473, // Tigrinya, Ethiopia | |
| 836 | ts = 0x0031, // Tsonga | |
| 837 | ts_za = 0x0431, // Tsonga, South Africa | |
| 838 | tr = 0x001F, // Turkish | |
| 839 | tr_tr = 0x041F, // Turkish, Turkey | |
| 840 | tk = 0x0042, // Turkmen | |
| 841 | tk_tm = 0x0442, // Turkmen, Turkmenistan | |
| 842 | uk = 0x0022, // Ukrainian | |
| 843 | uk_ua = 0x0422, // Ukrainian, Ukraine | |
| 844 | hsb = 0x002E, // Upper Sorbian | |
| 845 | hsb_de = 0x042E, // Upper Sorbian, Germany | |
| 846 | ur = 0x0020, // Urdu | |
| 847 | ur_in = 0x0820, // Urdu, India | |
| 848 | ur_pk = 0x0420, // Urdu, Islamic Republic of Pakistan | |
| 849 | ug = 0x0080, // Uyghur | |
| 850 | ug_cn = 0x0480, // Uyghur, People's Republic of China | |
| 851 | uz_cyrl = 0x7843, // Uzbek (Cyrillic) | |
| 852 | uz_cyrl_uz = 0x0843, // Uzbek (Cyrillic), Uzbekistan | |
| 853 | uz = 0x0043, // Uzbek (Latin) | |
| 854 | uz_latn = 0x7C43, // Uzbek (Latin) | |
| 855 | uz_latn_uz = 0x0443, // Uzbek (Latin), Uzbekistan | |
| 856 | ca_es_valencia = 0x0803, // Valencian, Spain | |
| 857 | ve = 0x0033, // Venda | |
| 858 | ve_za = 0x0433, // Venda, South Africa | |
| 859 | vi = 0x002A, // Vietnamese | |
| 860 | vi_vn = 0x042A, // Vietnamese, Vietnam | |
| 861 | cy = 0x0052, // Welsh | |
| 862 | cy_gb = 0x0452, // Welsh, United Kingdom | |
| 863 | wo = 0x0088, // Wolof | |
| 864 | wo_sn = 0x0488, // Wolof, Senegal | |
| 865 | xh = 0x0034, // Xhosa | |
| 866 | xh_za = 0x0434, // Xhosa, South Africa | |
| 867 | ii = 0x0078, // Yi | |
| 868 | ii_cn = 0x0478, // Yi, People's Republic of China | |
| 869 | yi_001 = 0x043D, // Yiddish, World | |
| 870 | yo = 0x006A, // Yoruba | |
| 871 | yo_ng = 0x046A, // Yoruba, Nigeria | |
| 872 | zu = 0x0035, // Zulu | |
| 873 | zu_za = 0x0435, // Zulu, South Africa | |
| 874 | ||
| 875 | /// Special case | |
| 876 | x_iv_mathan = 0x007F, // LANG_INVARIANT, "math alphanumeric sorting" | |
| 877 | }; |
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 | ||
| 7 | const std = @import("std"); | |
| 8 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 9 | const columnWidth = @import("literals.zig").columnWidth; | |
| 10 | const code_pages = @import("code_pages.zig"); | |
| 11 | const CodePage = code_pages.CodePage; | |
| 12 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 13 | const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit; | |
| 14 | ||
| 15 | const dumpTokensDuringTests = false; | |
| 16 | ||
| 17 | pub const default_max_string_literal_codepoints = 4097; | |
| 18 | ||
| 19 | pub const Token = struct { | |
| 20 | id: Id, | |
| 21 | start: usize, | |
| 22 | end: usize, | |
| 23 | line_number: usize, | |
| 24 | ||
| 25 | pub const Id = enum { | |
| 26 | literal, | |
| 27 | number, | |
| 28 | quoted_ascii_string, | |
| 29 | quoted_wide_string, | |
| 30 | operator, | |
| 31 | begin, | |
| 32 | end, | |
| 33 | comma, | |
| 34 | open_paren, | |
| 35 | close_paren, | |
| 36 | /// This Id is only used for errors, the Lexer will never return one | |
| 37 | /// of these from a `next` call. | |
| 38 | preprocessor_command, | |
| 39 | invalid, | |
| 40 | eof, | |
| 41 | ||
| 42 | pub fn nameForErrorDisplay(self: Id) []const u8 { | |
| 43 | return switch (self) { | |
| 44 | .literal => "<literal>", | |
| 45 | .number => "<number>", | |
| 46 | .quoted_ascii_string => "<quoted ascii string>", | |
| 47 | .quoted_wide_string => "<quoted wide string>", | |
| 48 | .operator => "<operator>", | |
| 49 | .begin => "<'{' or BEGIN>", | |
| 50 | .end => "<'}' or END>", | |
| 51 | .comma => ",", | |
| 52 | .open_paren => "(", | |
| 53 | .close_paren => ")", | |
| 54 | .preprocessor_command => "<preprocessor command>", | |
| 55 | .invalid => unreachable, | |
| 56 | .eof => "<eof>", | |
| 57 | }; | |
| 58 | } | |
| 59 | }; | |
| 60 | ||
| 61 | pub fn slice(self: Token, buffer: []const u8) []const u8 { | |
| 62 | return buffer[self.start..self.end]; | |
| 63 | } | |
| 64 | ||
| 65 | pub fn nameForErrorDisplay(self: Token, buffer: []const u8) []const u8 { | |
| 66 | return switch (self.id) { | |
| 67 | .eof => self.id.nameForErrorDisplay(), | |
| 68 | else => self.slice(buffer), | |
| 69 | }; | |
| 70 | } | |
| 71 | ||
| 72 | /// 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 | ||
| 129 | pub 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 | ||
| 187 | pub 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 | ||
| 209 | pub 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 | ||
| 1045 | fn 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 | ||
| 1057 | fn expectLexError(expected: LexError, actual: anytype) !void { | |
| 1058 | try std.testing.expectError(expected, actual); | |
| 1059 | if (dumpTokensDuringTests) std.debug.print("{!}\n", .{actual}); | |
| 1060 | } | |
| 1061 | ||
| 1062 | test "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 | ||
| 1069 | test "normal: string literals" { | |
| 1070 | try testLexNormal("\"\"", &.{.quoted_ascii_string}); | |
| 1071 | // "" is an escaped " | |
| 1072 | try testLexNormal("\" \"\" \"", &.{.quoted_ascii_string}); | |
| 1073 | } | |
| 1074 | ||
| 1075 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const code_pages = @import("code_pages.zig"); | |
| 3 | const CodePage = code_pages.CodePage; | |
| 4 | const windows1252 = @import("windows1252.zig"); | |
| 5 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 6 | const DiagnosticsContext = @import("errors.zig").DiagnosticsContext; | |
| 7 | const Token = @import("lex.zig").Token; | |
| 8 | ||
| 9 | /// rc is maximally liberal in terms of what it accepts as a number literal | |
| 10 | /// for data values. As long as it starts with a number or - or ~, that's good enough. | |
| 11 | pub fn isValidNumberDataLiteral(str: []const u8) bool { | |
| 12 | if (str.len == 0) return false; | |
| 13 | switch (str[0]) { | |
| 14 | '~', '-', '0'...'9' => return true, | |
| 15 | else => return false, | |
| 16 | } | |
| 17 | } | |
| 18 | ||
| 19 | pub const SourceBytes = struct { | |
| 20 | slice: []const u8, | |
| 21 | code_page: CodePage, | |
| 22 | }; | |
| 23 | ||
| 24 | pub const StringType = enum { ascii, wide }; | |
| 25 | ||
| 26 | /// Valid escapes: | |
| 27 | /// "" -> " | |
| 28 | /// \a, \A => 0x08 (not 0x07 like in C) | |
| 29 | /// \n => 0x0A | |
| 30 | /// \r => 0x0D | |
| 31 | /// \t, \T => 0x09 | |
| 32 | /// \\ => \ | |
| 33 | /// \nnn => byte with numeric value given by nnn interpreted as octal | |
| 34 | /// (wraps on overflow, number of digits can be 1-3 for ASCII strings | |
| 35 | /// and 1-7 for wide strings) | |
| 36 | /// \xhh => byte with numeric value given by hh interpreted as hex | |
| 37 | /// (number of digits can be 0-2 for ASCII strings and 0-4 for | |
| 38 | /// wide strings) | |
| 39 | /// \<\r+> => \ | |
| 40 | /// \<[\r\n\t ]+> => <nothing> | |
| 41 | /// | |
| 42 | /// Special cases: | |
| 43 | /// <\t> => 1-8 spaces, dependent on columns in the source rc file itself | |
| 44 | /// <\r> => <nothing> | |
| 45 | /// <\n+><\w+?\n?> => <space><\n> | |
| 46 | /// | |
| 47 | /// Special, especially weird case: | |
| 48 | /// \"" => " | |
| 49 | /// NOTE: This leads to footguns because the preprocessor can start parsing things | |
| 50 | /// out-of-sync with the RC compiler, expanding macros within string literals, etc. | |
| 51 | /// This parse function handles this case the same as the Windows RC compiler, but | |
| 52 | /// \" within a string literal is treated as an error by the lexer, so the relevant | |
| 53 | /// branches should never actually be hit during this function. | |
| 54 | pub const IterativeStringParser = struct { | |
| 55 | source: []const u8, | |
| 56 | code_page: CodePage, | |
| 57 | /// The type of the string inferred by the prefix (L"" or "") | |
| 58 | /// This is what matters for things like the maximum digits in an | |
| 59 | /// escape sequence, whether or not invalid escape sequences are skipped, etc. | |
| 60 | declared_string_type: StringType, | |
| 61 | pending_codepoint: ?u21 = null, | |
| 62 | num_pending_spaces: u8 = 0, | |
| 63 | index: usize = 0, | |
| 64 | column: usize = 0, | |
| 65 | diagnostics: ?DiagnosticsContext = null, | |
| 66 | seen_tab: bool = false, | |
| 67 | ||
| 68 | const State = enum { | |
| 69 | normal, | |
| 70 | quote, | |
| 71 | newline, | |
| 72 | escaped, | |
| 73 | escaped_cr, | |
| 74 | escaped_newlines, | |
| 75 | escaped_octal, | |
| 76 | escaped_hex, | |
| 77 | }; | |
| 78 | ||
| 79 | pub fn init(bytes: SourceBytes, options: StringParseOptions) IterativeStringParser { | |
| 80 | const declared_string_type: StringType = switch (bytes.slice[0]) { | |
| 81 | 'L', 'l' => .wide, | |
| 82 | else => .ascii, | |
| 83 | }; | |
| 84 | var source = bytes.slice[1 .. bytes.slice.len - 1]; // remove "" | |
| 85 | var column = options.start_column + 1; // for the removed " | |
| 86 | if (declared_string_type == .wide) { | |
| 87 | source = source[1..]; // remove L | |
| 88 | column += 1; // for the removed L | |
| 89 | } | |
| 90 | return .{ | |
| 91 | .source = source, | |
| 92 | .code_page = bytes.code_page, | |
| 93 | .declared_string_type = declared_string_type, | |
| 94 | .column = column, | |
| 95 | .diagnostics = options.diagnostics, | |
| 96 | }; | |
| 97 | } | |
| 98 | ||
| 99 | pub const ParsedCodepoint = struct { | |
| 100 | codepoint: u21, | |
| 101 | /// 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 | ||
| 367 | pub const StringParseOptions = struct { | |
| 368 | start_column: usize = 0, | |
| 369 | diagnostics: ?DiagnosticsContext = null, | |
| 370 | output_code_page: CodePage = .windows1252, | |
| 371 | }; | |
| 372 | ||
| 373 | pub 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 | ||
| 442 | pub 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 | ||
| 447 | pub 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 | ||
| 452 | pub 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 | ||
| 457 | test "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 | ||
| 597 | test "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 | ||
| 655 | test "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 | ||
| 714 | test "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 | ||
| 737 | test "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 | ||
| 771 | pub 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 | ||
| 777 | pub 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 | ||
| 784 | pub 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) | |
| 815 | pub 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 | ||
| 877 | test "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+719| ... | ... | @@ -0,0 +1,719 @@ |
| 1 | const std = @import("std"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const removeComments = @import("comments.zig").removeComments; | |
| 4 | const parseAndRemoveLineCommands = @import("source_mapping.zig").parseAndRemoveLineCommands; | |
| 5 | const compile = @import("compile.zig").compile; | |
| 6 | const Diagnostics = @import("errors.zig").Diagnostics; | |
| 7 | const cli = @import("cli.zig"); | |
| 8 | const preprocess = @import("preprocess.zig"); | |
| 9 | const renderErrorMessage = @import("utils.zig").renderErrorMessage; | |
| 10 | const aro = @import("aro"); | |
| 11 | ||
| 12 | pub fn main() !void { | |
| 13 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 14 | defer std.debug.assert(gpa.deinit() == .ok); | |
| 15 | const allocator = gpa.allocator(); | |
| 16 | ||
| 17 | var arena_state = std.heap.ArenaAllocator.init(allocator); | |
| 18 | defer arena_state.deinit(); | |
| 19 | const arena = arena_state.allocator(); | |
| 20 | ||
| 21 | const stderr = std.io.getStdErr(); | |
| 22 | const stderr_config = std.io.tty.detectConfig(stderr); | |
| 23 | ||
| 24 | const args = try std.process.argsAlloc(allocator); | |
| 25 | defer std.process.argsFree(allocator, args); | |
| 26 | ||
| 27 | if (args.len < 2) { | |
| 28 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "expected zig lib dir as first argument", .{}); | |
| 29 | std.os.exit(1); | |
| 30 | } | |
| 31 | const zig_lib_dir = args[1]; | |
| 32 | var cli_args = args[2..]; | |
| 33 | ||
| 34 | var zig_integration = false; | |
| 35 | if (cli_args.len > 0 and std.mem.eql(u8, cli_args[0], "--zig-integration")) { | |
| 36 | zig_integration = true; | |
| 37 | cli_args = args[3..]; | |
| 38 | } | |
| 39 | ||
| 40 | var error_handler: ErrorHandler = switch (zig_integration) { | |
| 41 | true => .{ | |
| 42 | .server = .{ | |
| 43 | .out = std.io.getStdOut(), | |
| 44 | .in = undefined, // won't be receiving messages | |
| 45 | .receive_fifo = undefined, // won't be receiving messages | |
| 46 | }, | |
| 47 | }, | |
| 48 | false => .{ | |
| 49 | .tty = stderr_config, | |
| 50 | }, | |
| 51 | }; | |
| 52 | ||
| 53 | if (zig_integration) { | |
| 54 | // Send progress with a special string to indicate that the building of the | |
| 55 | // resinator binary is finished and we've moved on to actually compiling the .rc file | |
| 56 | try error_handler.server.serveStringMessage(.progress, "<resinator>"); | |
| 57 | } | |
| 58 | ||
| 59 | var options = options: { | |
| 60 | var cli_diagnostics = cli.Diagnostics.init(allocator); | |
| 61 | defer cli_diagnostics.deinit(); | |
| 62 | var options = cli.parse(allocator, cli_args, &cli_diagnostics) catch |err| switch (err) { | |
| 63 | error.ParseError => { | |
| 64 | try error_handler.emitCliDiagnostics(allocator, cli_args, &cli_diagnostics); | |
| 65 | std.os.exit(1); | |
| 66 | }, | |
| 67 | else => |e| return e, | |
| 68 | }; | |
| 69 | try options.maybeAppendRC(std.fs.cwd()); | |
| 70 | ||
| 71 | if (!zig_integration) { | |
| 72 | // print any warnings/notes | |
| 73 | cli_diagnostics.renderToStdErr(args, stderr_config); | |
| 74 | // If there was something printed, then add an extra newline separator | |
| 75 | // so that there is a clear separation between the cli diagnostics and whatever | |
| 76 | // gets printed after | |
| 77 | if (cli_diagnostics.errors.items.len > 0) { | |
| 78 | try stderr.writeAll("\n"); | |
| 79 | } | |
| 80 | } | |
| 81 | break :options options; | |
| 82 | }; | |
| 83 | defer options.deinit(); | |
| 84 | ||
| 85 | if (options.print_help_and_exit) { | |
| 86 | try cli.writeUsage(stderr.writer(), "zig rc"); | |
| 87 | return; | |
| 88 | } | |
| 89 | ||
| 90 | // Don't allow verbose when integrating with Zig via stdout | |
| 91 | options.verbose = false; | |
| 92 | ||
| 93 | const stdout_writer = std.io.getStdOut().writer(); | |
| 94 | if (options.verbose) { | |
| 95 | try options.dumpVerbose(stdout_writer); | |
| 96 | try stdout_writer.writeByte('\n'); | |
| 97 | } | |
| 98 | ||
| 99 | var dependencies_list = std.ArrayList([]const u8).init(allocator); | |
| 100 | defer { | |
| 101 | for (dependencies_list.items) |item| { | |
| 102 | allocator.free(item); | |
| 103 | } | |
| 104 | dependencies_list.deinit(); | |
| 105 | } | |
| 106 | const maybe_dependencies_list: ?*std.ArrayList([]const u8) = if (options.depfile_path != null) &dependencies_list else null; | |
| 107 | ||
| 108 | const include_paths = getIncludePaths(arena, options.auto_includes, zig_lib_dir) catch |err| switch (err) { | |
| 109 | error.OutOfMemory => |e| return e, | |
| 110 | else => |e| { | |
| 111 | switch (e) { | |
| 112 | error.MsvcIncludesNotFound => { | |
| 113 | try error_handler.emitMessage(allocator, .err, "MSVC include paths could not be automatically detected", .{}); | |
| 114 | }, | |
| 115 | error.MingwIncludesNotFound => { | |
| 116 | try error_handler.emitMessage(allocator, .err, "MinGW include paths could not be automatically detected", .{}); | |
| 117 | }, | |
| 118 | } | |
| 119 | try error_handler.emitMessage(allocator, .note, "to disable auto includes, use the option /:auto-includes none", .{}); | |
| 120 | std.os.exit(1); | |
| 121 | }, | |
| 122 | }; | |
| 123 | ||
| 124 | const full_input = full_input: { | |
| 125 | if (options.preprocess != .no) { | |
| 126 | var preprocessed_buf = std.ArrayList(u8).init(allocator); | |
| 127 | errdefer preprocessed_buf.deinit(); | |
| 128 | ||
| 129 | // We're going to throw away everything except the final preprocessed output anyway, | |
| 130 | // so we can use a scoped arena for everything else. | |
| 131 | var aro_arena_state = std.heap.ArenaAllocator.init(allocator); | |
| 132 | defer aro_arena_state.deinit(); | |
| 133 | const aro_arena = aro_arena_state.allocator(); | |
| 134 | ||
| 135 | var comp = aro.Compilation.init(aro_arena); | |
| 136 | defer comp.deinit(); | |
| 137 | ||
| 138 | var argv = std.ArrayList([]const u8).init(comp.gpa); | |
| 139 | defer argv.deinit(); | |
| 140 | ||
| 141 | try argv.append("arocc"); // dummy command name | |
| 142 | try preprocess.appendAroArgs(aro_arena, &argv, options, include_paths); | |
| 143 | try argv.append(options.input_filename); | |
| 144 | ||
| 145 | if (options.verbose) { | |
| 146 | try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n"); | |
| 147 | for (argv.items[0 .. argv.items.len - 1]) |arg| { | |
| 148 | try stdout_writer.print("{s} ", .{arg}); | |
| 149 | } | |
| 150 | try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]}); | |
| 151 | } | |
| 152 | ||
| 153 | preprocess.preprocess(&comp, preprocessed_buf.writer(), argv.items, maybe_dependencies_list) catch |err| switch (err) { | |
| 154 | error.GeneratedSourceError => { | |
| 155 | try error_handler.emitAroDiagnostics(allocator, "failed during preprocessor setup (this is always a bug):", &comp); | |
| 156 | std.os.exit(1); | |
| 157 | }, | |
| 158 | // ArgError can occur if e.g. the .rc file is not found | |
| 159 | error.ArgError, error.PreprocessError => { | |
| 160 | try error_handler.emitAroDiagnostics(allocator, "failed during preprocessing:", &comp); | |
| 161 | std.os.exit(1); | |
| 162 | }, | |
| 163 | error.StreamTooLong => { | |
| 164 | try error_handler.emitMessage(allocator, .err, "failed during preprocessing: maximum file size exceeded", .{}); | |
| 165 | std.os.exit(1); | |
| 166 | }, | |
| 167 | error.OutOfMemory => |e| return e, | |
| 168 | }; | |
| 169 | ||
| 170 | break :full_input try preprocessed_buf.toOwnedSlice(); | |
| 171 | } else { | |
| 172 | break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| { | |
| 173 | try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) }); | |
| 174 | std.os.exit(1); | |
| 175 | }; | |
| 176 | } | |
| 177 | }; | |
| 178 | defer allocator.free(full_input); | |
| 179 | ||
| 180 | if (options.preprocess == .only) { | |
| 181 | try std.fs.cwd().writeFile(options.output_filename, full_input); | |
| 182 | return; | |
| 183 | } | |
| 184 | ||
| 185 | // Note: We still want to run this when no-preprocess is set because: | |
| 186 | // 1. We want to print accurate line numbers after removing multiline comments | |
| 187 | // 2. We want to be able to handle an already-preprocessed input with #line commands in it | |
| 188 | var mapping_results = try parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }); | |
| 189 | defer mapping_results.mappings.deinit(allocator); | |
| 190 | ||
| 191 | const final_input = removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings) catch |err| switch (err) { | |
| 192 | error.InvalidSourceMappingCollapse => { | |
| 193 | try error_handler.emitMessage(allocator, .err, "failed during comment removal; this is a known bug", .{}); | |
| 194 | std.os.exit(1); | |
| 195 | }, | |
| 196 | else => |e| return e, | |
| 197 | }; | |
| 198 | ||
| 199 | var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| { | |
| 200 | try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) }); | |
| 201 | std.os.exit(1); | |
| 202 | }; | |
| 203 | var output_file_closed = false; | |
| 204 | defer if (!output_file_closed) output_file.close(); | |
| 205 | ||
| 206 | var diagnostics = Diagnostics.init(allocator); | |
| 207 | defer diagnostics.deinit(); | |
| 208 | ||
| 209 | var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); | |
| 210 | ||
| 211 | compile(allocator, final_input, output_buffered_stream.writer(), .{ | |
| 212 | .cwd = std.fs.cwd(), | |
| 213 | .diagnostics = &diagnostics, | |
| 214 | .source_mappings = &mapping_results.mappings, | |
| 215 | .dependencies_list = maybe_dependencies_list, | |
| 216 | .ignore_include_env_var = options.ignore_include_env_var, | |
| 217 | .extra_include_paths = options.extra_include_paths.items, | |
| 218 | .system_include_paths = include_paths, | |
| 219 | .default_language_id = options.default_language_id, | |
| 220 | .default_code_page = options.default_code_page orelse .windows1252, | |
| 221 | .verbose = options.verbose, | |
| 222 | .null_terminate_string_table_strings = options.null_terminate_string_table_strings, | |
| 223 | .max_string_literal_codepoints = options.max_string_literal_codepoints, | |
| 224 | .silent_duplicate_control_ids = options.silent_duplicate_control_ids, | |
| 225 | .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, | |
| 226 | }) catch |err| switch (err) { | |
| 227 | error.ParseError, error.CompileError => { | |
| 228 | try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); | |
| 229 | // Delete the output file on error | |
| 230 | output_file.close(); | |
| 231 | output_file_closed = true; | |
| 232 | // Failing to delete is not really a big deal, so swallow any errors | |
| 233 | std.fs.cwd().deleteFile(options.output_filename) catch {}; | |
| 234 | std.os.exit(1); | |
| 235 | }, | |
| 236 | else => |e| return e, | |
| 237 | }; | |
| 238 | ||
| 239 | try output_buffered_stream.flush(); | |
| 240 | ||
| 241 | // print any warnings/notes | |
| 242 | if (!zig_integration) { | |
| 243 | diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); | |
| 244 | } | |
| 245 | ||
| 246 | // write the depfile | |
| 247 | if (options.depfile_path) |depfile_path| { | |
| 248 | var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { | |
| 249 | try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); | |
| 250 | std.os.exit(1); | |
| 251 | }; | |
| 252 | defer depfile.close(); | |
| 253 | ||
| 254 | const depfile_writer = depfile.writer(); | |
| 255 | var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer); | |
| 256 | switch (options.depfile_fmt) { | |
| 257 | .json => { | |
| 258 | var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 }); | |
| 259 | defer write_stream.deinit(); | |
| 260 | ||
| 261 | try write_stream.beginArray(); | |
| 262 | for (dependencies_list.items) |dep_path| { | |
| 263 | try write_stream.write(dep_path); | |
| 264 | } | |
| 265 | try write_stream.endArray(); | |
| 266 | }, | |
| 267 | } | |
| 268 | try depfile_buffered_writer.flush(); | |
| 269 | } | |
| 270 | } | |
| 271 | ||
| 272 | fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 { | |
| 273 | var includes = auto_includes_option; | |
| 274 | if (builtin.target.os.tag != .windows) { | |
| 275 | switch (includes) { | |
| 276 | // MSVC can't be found when the host isn't Windows, so short-circuit. | |
| 277 | .msvc => return error.MsvcIncludesNotFound, | |
| 278 | // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts. | |
| 279 | .any => includes = .gnu, | |
| 280 | .none, .gnu => {}, | |
| 281 | } | |
| 282 | } | |
| 283 | ||
| 284 | while (true) { | |
| 285 | switch (includes) { | |
| 286 | .none => return &[_][]const u8{}, | |
| 287 | .any, .msvc => { | |
| 288 | // MSVC is only detectable on Windows targets. This unreachable is to signify | |
| 289 | // that .any and .msvc should be dealt with on non-Windows targets before this point, | |
| 290 | // since getting MSVC include paths uses Windows-only APIs. | |
| 291 | if (builtin.target.os.tag != .windows) unreachable; | |
| 292 | ||
| 293 | const target_query: std.Target.Query = .{ | |
| 294 | .os_tag = .windows, | |
| 295 | .abi = .msvc, | |
| 296 | }; | |
| 297 | const target = std.zig.resolveTargetQueryOrFatal(target_query); | |
| 298 | const is_native_abi = target_query.isNativeAbi(); | |
| 299 | const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch { | |
| 300 | if (includes == .any) { | |
| 301 | // fall back to mingw | |
| 302 | includes = .gnu; | |
| 303 | continue; | |
| 304 | } | |
| 305 | return error.MsvcIncludesNotFound; | |
| 306 | }; | |
| 307 | if (detected_libc.libc_include_dir_list.len == 0) { | |
| 308 | if (includes == .any) { | |
| 309 | // fall back to mingw | |
| 310 | includes = .gnu; | |
| 311 | continue; | |
| 312 | } | |
| 313 | return error.MsvcIncludesNotFound; | |
| 314 | } | |
| 315 | return detected_libc.libc_include_dir_list; | |
| 316 | }, | |
| 317 | .gnu => { | |
| 318 | const target_query: std.Target.Query = .{ | |
| 319 | .os_tag = .windows, | |
| 320 | .abi = .gnu, | |
| 321 | }; | |
| 322 | const target = std.zig.resolveTargetQueryOrFatal(target_query); | |
| 323 | const is_native_abi = target_query.isNativeAbi(); | |
| 324 | const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| switch (err) { | |
| 325 | error.OutOfMemory => |e| return e, | |
| 326 | else => return error.MingwIncludesNotFound, | |
| 327 | }; | |
| 328 | return detected_libc.libc_include_dir_list; | |
| 329 | }, | |
| 330 | } | |
| 331 | } | |
| 332 | } | |
| 333 | ||
| 334 | const ErrorBundle = std.zig.ErrorBundle; | |
| 335 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 336 | ||
| 337 | const ErrorHandler = union(enum) { | |
| 338 | server: std.zig.Server, | |
| 339 | tty: std.io.tty.Config, | |
| 340 | ||
| 341 | pub fn emitCliDiagnostics( | |
| 342 | self: *ErrorHandler, | |
| 343 | allocator: std.mem.Allocator, | |
| 344 | args: []const []const u8, | |
| 345 | diagnostics: *cli.Diagnostics, | |
| 346 | ) !void { | |
| 347 | switch (self.*) { | |
| 348 | .server => |*server| { | |
| 349 | var error_bundle = try cliDiagnosticsToErrorBundle(allocator, diagnostics); | |
| 350 | defer error_bundle.deinit(allocator); | |
| 351 | ||
| 352 | try server.serveErrorBundle(error_bundle); | |
| 353 | }, | |
| 354 | .tty => { | |
| 355 | diagnostics.renderToStdErr(args, self.tty); | |
| 356 | }, | |
| 357 | } | |
| 358 | } | |
| 359 | ||
| 360 | pub fn emitAroDiagnostics( | |
| 361 | self: *ErrorHandler, | |
| 362 | allocator: std.mem.Allocator, | |
| 363 | fail_msg: []const u8, | |
| 364 | comp: *aro.Compilation, | |
| 365 | ) !void { | |
| 366 | switch (self.*) { | |
| 367 | .server => |*server| { | |
| 368 | var error_bundle = try aroDiagnosticsToErrorBundle(allocator, fail_msg, comp); | |
| 369 | defer error_bundle.deinit(allocator); | |
| 370 | ||
| 371 | try server.serveErrorBundle(error_bundle); | |
| 372 | }, | |
| 373 | .tty => { | |
| 374 | // extra newline to separate this line from the aro errors | |
| 375 | try renderErrorMessage(std.io.getStdErr().writer(), self.tty, .err, "{s}\n", .{fail_msg}); | |
| 376 | aro.Diagnostics.render(comp, self.tty); | |
| 377 | }, | |
| 378 | } | |
| 379 | } | |
| 380 | ||
| 381 | pub fn emitDiagnostics( | |
| 382 | self: *ErrorHandler, | |
| 383 | allocator: std.mem.Allocator, | |
| 384 | cwd: std.fs.Dir, | |
| 385 | source: []const u8, | |
| 386 | diagnostics: *Diagnostics, | |
| 387 | mappings: SourceMappings, | |
| 388 | ) !void { | |
| 389 | switch (self.*) { | |
| 390 | .server => |*server| { | |
| 391 | var error_bundle = try diagnosticsToErrorBundle(allocator, source, diagnostics, mappings); | |
| 392 | defer error_bundle.deinit(allocator); | |
| 393 | ||
| 394 | try server.serveErrorBundle(error_bundle); | |
| 395 | }, | |
| 396 | .tty => { | |
| 397 | diagnostics.renderToStdErr(cwd, source, self.tty, mappings); | |
| 398 | }, | |
| 399 | } | |
| 400 | } | |
| 401 | ||
| 402 | pub fn emitMessage( | |
| 403 | self: *ErrorHandler, | |
| 404 | allocator: std.mem.Allocator, | |
| 405 | msg_type: @import("utils.zig").ErrorMessageType, | |
| 406 | comptime format: []const u8, | |
| 407 | args: anytype, | |
| 408 | ) !void { | |
| 409 | switch (self.*) { | |
| 410 | .server => |*server| { | |
| 411 | // only emit errors | |
| 412 | if (msg_type != .err) return; | |
| 413 | ||
| 414 | var error_bundle = try errorStringToErrorBundle(allocator, format, args); | |
| 415 | defer error_bundle.deinit(allocator); | |
| 416 | ||
| 417 | try server.serveErrorBundle(error_bundle); | |
| 418 | }, | |
| 419 | .tty => { | |
| 420 | try renderErrorMessage(std.io.getStdErr().writer(), self.tty, msg_type, format, args); | |
| 421 | }, | |
| 422 | } | |
| 423 | } | |
| 424 | }; | |
| 425 | ||
| 426 | fn cliDiagnosticsToErrorBundle( | |
| 427 | gpa: std.mem.Allocator, | |
| 428 | diagnostics: *cli.Diagnostics, | |
| 429 | ) !ErrorBundle { | |
| 430 | @setCold(true); | |
| 431 | ||
| 432 | var bundle: ErrorBundle.Wip = undefined; | |
| 433 | try bundle.init(gpa); | |
| 434 | errdefer bundle.deinit(); | |
| 435 | ||
| 436 | try bundle.addRootErrorMessage(.{ | |
| 437 | .msg = try bundle.addString("invalid command line option(s)"), | |
| 438 | }); | |
| 439 | ||
| 440 | var cur_err: ?ErrorBundle.ErrorMessage = null; | |
| 441 | var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{}; | |
| 442 | defer cur_notes.deinit(gpa); | |
| 443 | for (diagnostics.errors.items) |err_details| { | |
| 444 | switch (err_details.type) { | |
| 445 | .err => { | |
| 446 | if (cur_err) |err| { | |
| 447 | try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items); | |
| 448 | } | |
| 449 | cur_err = .{ | |
| 450 | .msg = try bundle.addString(err_details.msg.items), | |
| 451 | }; | |
| 452 | cur_notes.clearRetainingCapacity(); | |
| 453 | }, | |
| 454 | .warning => cur_err = null, | |
| 455 | .note => { | |
| 456 | if (cur_err == null) continue; | |
| 457 | cur_err.?.notes_len += 1; | |
| 458 | try cur_notes.append(gpa, .{ | |
| 459 | .msg = try bundle.addString(err_details.msg.items), | |
| 460 | }); | |
| 461 | }, | |
| 462 | } | |
| 463 | } | |
| 464 | if (cur_err) |err| { | |
| 465 | try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items); | |
| 466 | } | |
| 467 | ||
| 468 | return try bundle.toOwnedBundle(""); | |
| 469 | } | |
| 470 | ||
| 471 | fn diagnosticsToErrorBundle( | |
| 472 | gpa: std.mem.Allocator, | |
| 473 | source: []const u8, | |
| 474 | diagnostics: *Diagnostics, | |
| 475 | mappings: SourceMappings, | |
| 476 | ) !ErrorBundle { | |
| 477 | @setCold(true); | |
| 478 | ||
| 479 | var bundle: ErrorBundle.Wip = undefined; | |
| 480 | try bundle.init(gpa); | |
| 481 | errdefer bundle.deinit(); | |
| 482 | ||
| 483 | var msg_buf: std.ArrayListUnmanaged(u8) = .{}; | |
| 484 | defer msg_buf.deinit(gpa); | |
| 485 | var cur_err: ?ErrorBundle.ErrorMessage = null; | |
| 486 | var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{}; | |
| 487 | defer cur_notes.deinit(gpa); | |
| 488 | for (diagnostics.errors.items) |err_details| { | |
| 489 | switch (err_details.type) { | |
| 490 | .hint => continue, | |
| 491 | // Clear the current error so that notes don't bleed into unassociated errors | |
| 492 | .warning => { | |
| 493 | cur_err = null; | |
| 494 | continue; | |
| 495 | }, | |
| 496 | .note => if (cur_err == null) continue, | |
| 497 | .err => {}, | |
| 498 | } | |
| 499 | const corresponding_span = mappings.getCorrespondingSpan(err_details.token.line_number).?; | |
| 500 | const err_line = corresponding_span.start_line; | |
| 501 | const err_filename = mappings.files.get(corresponding_span.filename_offset); | |
| 502 | ||
| 503 | const source_line_start = err_details.token.getLineStartForErrorDisplay(source); | |
| 504 | // Treat tab stops as 1 column wide for error display purposes, | |
| 505 | // and add one to get a 1-based column | |
| 506 | const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1; | |
| 507 | ||
| 508 | msg_buf.clearRetainingCapacity(); | |
| 509 | try err_details.render(msg_buf.writer(gpa), source, diagnostics.strings.items); | |
| 510 | ||
| 511 | const src_loc = src_loc: { | |
| 512 | var src_loc: ErrorBundle.SourceLocation = .{ | |
| 513 | .src_path = try bundle.addString(err_filename), | |
| 514 | .line = @intCast(err_line - 1), // 1-based -> 0-based | |
| 515 | .column = @intCast(column - 1), // 1-based -> 0-based | |
| 516 | .span_start = 0, | |
| 517 | .span_main = 0, | |
| 518 | .span_end = 0, | |
| 519 | }; | |
| 520 | if (err_details.print_source_line) { | |
| 521 | const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start); | |
| 522 | const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len); | |
| 523 | src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len); | |
| 524 | src_loc.span_main = @intCast(visual_info.point_offset); | |
| 525 | src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len); | |
| 526 | src_loc.source_line = try bundle.addString(source_line); | |
| 527 | } | |
| 528 | break :src_loc try bundle.addSourceLocation(src_loc); | |
| 529 | }; | |
| 530 | ||
| 531 | switch (err_details.type) { | |
| 532 | .err => { | |
| 533 | if (cur_err) |err| { | |
| 534 | try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items); | |
| 535 | } | |
| 536 | cur_err = .{ | |
| 537 | .msg = try bundle.addString(msg_buf.items), | |
| 538 | .src_loc = src_loc, | |
| 539 | }; | |
| 540 | cur_notes.clearRetainingCapacity(); | |
| 541 | }, | |
| 542 | .note => { | |
| 543 | cur_err.?.notes_len += 1; | |
| 544 | try cur_notes.append(gpa, .{ | |
| 545 | .msg = try bundle.addString(msg_buf.items), | |
| 546 | .src_loc = src_loc, | |
| 547 | }); | |
| 548 | }, | |
| 549 | .warning, .hint => unreachable, | |
| 550 | } | |
| 551 | } | |
| 552 | if (cur_err) |err| { | |
| 553 | try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items); | |
| 554 | } | |
| 555 | ||
| 556 | return try bundle.toOwnedBundle(""); | |
| 557 | } | |
| 558 | ||
| 559 | fn flushErrorMessageIntoBundle(wip: *ErrorBundle.Wip, msg: ErrorBundle.ErrorMessage, notes: []const ErrorBundle.ErrorMessage) !void { | |
| 560 | try wip.addRootErrorMessage(msg); | |
| 561 | const notes_start = try wip.reserveNotes(@intCast(notes.len)); | |
| 562 | for (notes_start.., notes) |i, note| { | |
| 563 | wip.extra.items[i] = @intFromEnum(wip.addErrorMessageAssumeCapacity(note)); | |
| 564 | } | |
| 565 | } | |
| 566 | ||
| 567 | fn errorStringToErrorBundle(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) !ErrorBundle { | |
| 568 | @setCold(true); | |
| 569 | var bundle: ErrorBundle.Wip = undefined; | |
| 570 | try bundle.init(allocator); | |
| 571 | errdefer bundle.deinit(); | |
| 572 | try bundle.addRootErrorMessage(.{ | |
| 573 | .msg = try bundle.printString(format, args), | |
| 574 | }); | |
| 575 | return try bundle.toOwnedBundle(""); | |
| 576 | } | |
| 577 | ||
| 578 | fn aroDiagnosticsToErrorBundle( | |
| 579 | gpa: std.mem.Allocator, | |
| 580 | fail_msg: []const u8, | |
| 581 | comp: *aro.Compilation, | |
| 582 | ) !ErrorBundle { | |
| 583 | @setCold(true); | |
| 584 | ||
| 585 | var bundle: ErrorBundle.Wip = undefined; | |
| 586 | try bundle.init(gpa); | |
| 587 | errdefer bundle.deinit(); | |
| 588 | ||
| 589 | try bundle.addRootErrorMessage(.{ | |
| 590 | .msg = try bundle.addString(fail_msg), | |
| 591 | }); | |
| 592 | ||
| 593 | var msg_writer = MsgWriter.init(gpa); | |
| 594 | defer msg_writer.deinit(); | |
| 595 | var cur_err: ?ErrorBundle.ErrorMessage = null; | |
| 596 | var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{}; | |
| 597 | defer cur_notes.deinit(gpa); | |
| 598 | for (comp.diagnostics.list.items) |msg| { | |
| 599 | switch (msg.kind) { | |
| 600 | // Clear the current error so that notes don't bleed into unassociated errors | |
| 601 | .off, .warning => { | |
| 602 | cur_err = null; | |
| 603 | continue; | |
| 604 | }, | |
| 605 | .note => if (cur_err == null) continue, | |
| 606 | .@"fatal error", .@"error" => {}, | |
| 607 | .default => unreachable, | |
| 608 | } | |
| 609 | msg_writer.resetRetainingCapacity(); | |
| 610 | aro.Diagnostics.renderMessage(comp, &msg_writer, msg); | |
| 611 | ||
| 612 | const src_loc = src_loc: { | |
| 613 | if (msg_writer.path) |src_path| { | |
| 614 | var src_loc: ErrorBundle.SourceLocation = .{ | |
| 615 | .src_path = try bundle.addString(src_path), | |
| 616 | .line = msg_writer.line - 1, // 1-based -> 0-based | |
| 617 | .column = msg_writer.col - 1, // 1-based -> 0-based | |
| 618 | .span_start = 0, | |
| 619 | .span_main = 0, | |
| 620 | .span_end = 0, | |
| 621 | }; | |
| 622 | if (msg_writer.source_line) |source_line| { | |
| 623 | src_loc.span_start = msg_writer.span_main; | |
| 624 | src_loc.span_main = msg_writer.span_main; | |
| 625 | src_loc.span_end = msg_writer.span_main; | |
| 626 | src_loc.source_line = try bundle.addString(source_line); | |
| 627 | } | |
| 628 | break :src_loc try bundle.addSourceLocation(src_loc); | |
| 629 | } | |
| 630 | break :src_loc ErrorBundle.SourceLocationIndex.none; | |
| 631 | }; | |
| 632 | ||
| 633 | switch (msg.kind) { | |
| 634 | .@"fatal error", .@"error" => { | |
| 635 | if (cur_err) |err| { | |
| 636 | try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items); | |
| 637 | } | |
| 638 | cur_err = .{ | |
| 639 | .msg = try bundle.addString(msg_writer.buf.items), | |
| 640 | .src_loc = src_loc, | |
| 641 | }; | |
| 642 | cur_notes.clearRetainingCapacity(); | |
| 643 | }, | |
| 644 | .note => { | |
| 645 | cur_err.?.notes_len += 1; | |
| 646 | try cur_notes.append(gpa, .{ | |
| 647 | .msg = try bundle.addString(msg_writer.buf.items), | |
| 648 | .src_loc = src_loc, | |
| 649 | }); | |
| 650 | }, | |
| 651 | .off, .warning, .default => unreachable, | |
| 652 | } | |
| 653 | } | |
| 654 | if (cur_err) |err| { | |
| 655 | try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items); | |
| 656 | } | |
| 657 | ||
| 658 | return try bundle.toOwnedBundle(""); | |
| 659 | } | |
| 660 | ||
| 661 | // Similar to aro.Diagnostics.MsgWriter but: | |
| 662 | // - Writers to an ArrayList | |
| 663 | // - Only prints the message itself (no location, source line, error: prefix, etc) | |
| 664 | // - Keeps track of source path/line/col instead | |
| 665 | const MsgWriter = struct { | |
| 666 | buf: std.ArrayList(u8), | |
| 667 | path: ?[]const u8 = null, | |
| 668 | // 1-indexed | |
| 669 | line: u32 = undefined, | |
| 670 | col: u32 = undefined, | |
| 671 | source_line: ?[]const u8 = null, | |
| 672 | span_main: u32 = undefined, | |
| 673 | ||
| 674 | fn init(allocator: std.mem.Allocator) MsgWriter { | |
| 675 | return .{ | |
| 676 | .buf = std.ArrayList(u8).init(allocator), | |
| 677 | }; | |
| 678 | } | |
| 679 | ||
| 680 | fn deinit(m: *MsgWriter) void { | |
| 681 | m.buf.deinit(); | |
| 682 | } | |
| 683 | ||
| 684 | fn resetRetainingCapacity(m: *MsgWriter) void { | |
| 685 | m.buf.clearRetainingCapacity(); | |
| 686 | m.path = null; | |
| 687 | m.source_line = null; | |
| 688 | } | |
| 689 | ||
| 690 | pub fn print(m: *MsgWriter, comptime fmt: []const u8, args: anytype) void { | |
| 691 | m.buf.writer().print(fmt, args) catch {}; | |
| 692 | } | |
| 693 | ||
| 694 | pub fn write(m: *MsgWriter, msg: []const u8) void { | |
| 695 | m.buf.writer().writeAll(msg) catch {}; | |
| 696 | } | |
| 697 | ||
| 698 | pub fn setColor(m: *MsgWriter, color: std.io.tty.Color) void { | |
| 699 | _ = m; | |
| 700 | _ = color; | |
| 701 | } | |
| 702 | ||
| 703 | pub fn location(m: *MsgWriter, path: []const u8, line: u32, col: u32) void { | |
| 704 | m.path = path; | |
| 705 | m.line = line; | |
| 706 | m.col = col; | |
| 707 | } | |
| 708 | ||
| 709 | pub fn start(m: *MsgWriter, kind: aro.Diagnostics.Kind) void { | |
| 710 | _ = m; | |
| 711 | _ = kind; | |
| 712 | } | |
| 713 | ||
| 714 | pub fn end(m: *MsgWriter, maybe_line: ?[]const u8, col: u32, end_with_splice: bool) void { | |
| 715 | _ = end_with_splice; | |
| 716 | m.source_line = maybe_line; | |
| 717 | m.span_main = col; | |
| 718 | } | |
| 719 | }; |
lib/compiler/resinator/parse.zig created+1897| ... | ... | @@ -0,0 +1,1897 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Lexer = @import("lex.zig").Lexer; | |
| 3 | const Token = @import("lex.zig").Token; | |
| 4 | const Node = @import("ast.zig").Node; | |
| 5 | const Tree = @import("ast.zig").Tree; | |
| 6 | const CodePageLookup = @import("ast.zig").CodePageLookup; | |
| 7 | const Resource = @import("rc.zig").Resource; | |
| 8 | const Allocator = std.mem.Allocator; | |
| 9 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 10 | const Diagnostics = @import("errors.zig").Diagnostics; | |
| 11 | const SourceBytes = @import("literals.zig").SourceBytes; | |
| 12 | const Compiler = @import("compile.zig").Compiler; | |
| 13 | const rc = @import("rc.zig"); | |
| 14 | const res = @import("res.zig"); | |
| 15 | ||
| 16 | // TODO: Make these configurable? | |
| 17 | pub const max_nested_menu_level: u32 = 512; | |
| 18 | pub const max_nested_version_level: u32 = 512; | |
| 19 | pub const max_nested_expression_level: u32 = 200; | |
| 20 | ||
| 21 | pub const Parser = struct { | |
| 22 | const Self = @This(); | |
| 23 | ||
| 24 | lexer: *Lexer, | |
| 25 | /// values that need to be initialized per-parse | |
| 26 | state: Parser.State = undefined, | |
| 27 | options: Parser.Options, | |
| 28 | ||
| 29 | pub const Error = error{ParseError} || Allocator.Error; | |
| 30 | ||
| 31 | pub const Options = struct { | |
| 32 | warn_instead_of_error_on_invalid_code_page: bool = false, | |
| 33 | }; | |
| 34 | ||
| 35 | pub fn init(lexer: *Lexer, options: Options) Parser { | |
| 36 | return Parser{ | |
| 37 | .lexer = lexer, | |
| 38 | .options = options, | |
| 39 | }; | |
| 40 | } | |
| 41 | ||
| 42 | pub const State = struct { | |
| 43 | token: Token, | |
| 44 | lookahead_lexer: Lexer, | |
| 45 | allocator: Allocator, | |
| 46 | arena: Allocator, | |
| 47 | diagnostics: *Diagnostics, | |
| 48 | input_code_page_lookup: CodePageLookup, | |
| 49 | output_code_page_lookup: CodePageLookup, | |
| 50 | }; | |
| 51 | ||
| 52 | pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree { | |
| 53 | var arena = std.heap.ArenaAllocator.init(allocator); | |
| 54 | errdefer arena.deinit(); | |
| 55 | ||
| 56 | self.state = Parser.State{ | |
| 57 | .token = undefined, | |
| 58 | .lookahead_lexer = undefined, | |
| 59 | .allocator = allocator, | |
| 60 | .arena = arena.allocator(), | |
| 61 | .diagnostics = diagnostics, | |
| 62 | .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), | |
| 63 | .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), | |
| 64 | }; | |
| 65 | ||
| 66 | const parsed_root = try self.parseRoot(); | |
| 67 | ||
| 68 | const tree = try self.state.arena.create(Tree); | |
| 69 | tree.* = .{ | |
| 70 | .node = parsed_root, | |
| 71 | .input_code_pages = self.state.input_code_page_lookup, | |
| 72 | .output_code_pages = self.state.output_code_page_lookup, | |
| 73 | .source = self.lexer.buffer, | |
| 74 | .arena = arena.state, | |
| 75 | .allocator = allocator, | |
| 76 | }; | |
| 77 | return tree; | |
| 78 | } | |
| 79 | ||
| 80 | fn parseRoot(self: *Self) Error!*Node { | |
| 81 | var statements = std.ArrayList(*Node).init(self.state.allocator); | |
| 82 | defer statements.deinit(); | |
| 83 | ||
| 84 | try self.parseStatements(&statements); | |
| 85 | try self.check(.eof); | |
| 86 | ||
| 87 | const node = try self.state.arena.create(Node.Root); | |
| 88 | node.* = .{ | |
| 89 | .body = try self.state.arena.dupe(*Node, statements.items), | |
| 90 | }; | |
| 91 | return &node.base; | |
| 92 | } | |
| 93 | ||
| 94 | fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void { | |
| 95 | while (true) { | |
| 96 | try self.nextToken(.whitespace_delimiter_only); | |
| 97 | if (self.state.token.id == .eof) break; | |
| 98 | // The Win32 compiler will sometimes try to recover from errors | |
| 99 | // and then restart parsing afterwards. We don't ever do this | |
| 100 | // because it almost always leads to unhelpful error messages | |
| 101 | // (usually it will end up with bogus things like 'file | |
| 102 | // not found: {') | |
| 103 | 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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const Allocator = std.mem.Allocator; | |
| 4 | const cli = @import("cli.zig"); | |
| 5 | const aro = @import("aro"); | |
| 6 | ||
| 7 | const PreprocessError = error{ ArgError, GeneratedSourceError, PreprocessError, StreamTooLong, OutOfMemory }; | |
| 8 | ||
| 9 | pub 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 | ||
| 75 | fn 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`. | |
| 90 | pub 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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const utils = @import("utils.zig"); | |
| 3 | const res = @import("res.zig"); | |
| 4 | const SourceBytes = @import("literals.zig").SourceBytes; | |
| 5 | ||
| 6 | // https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files | |
| 7 | ||
| 8 | pub const Resource = enum { | |
| 9 | accelerators, | |
| 10 | bitmap, | |
| 11 | cursor, | |
| 12 | dialog, | |
| 13 | dialogex, | |
| 14 | /// As far as I can tell, this is undocumented; the most I could find was this: | |
| 15 | /// https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/91697 | |
| 16 | dlginclude, | |
| 17 | /// Undocumented, basically works exactly like RCDATA | |
| 18 | dlginit, | |
| 19 | font, | |
| 20 | html, | |
| 21 | icon, | |
| 22 | menu, | |
| 23 | menuex, | |
| 24 | messagetable, | |
| 25 | plugplay, // Obsolete | |
| 26 | rcdata, | |
| 27 | stringtable, | |
| 28 | /// Undocumented | |
| 29 | toolbar, | |
| 30 | user_defined, | |
| 31 | versioninfo, | |
| 32 | vxd, // Obsolete | |
| 33 | ||
| 34 | // Types that are treated as a user-defined type when encountered, but have | |
| 35 | // special meaning without the Visual Studio GUI. We match the Win32 RC compiler | |
| 36 | // behavior by acting as if these keyword don't exist when compiling the .rc | |
| 37 | // (thereby treating them as user-defined). | |
| 38 | //textinclude, // A special resource that is interpreted by Visual C++. | |
| 39 | //typelib, // A special resource that is used with the /TLBID and /TLBOUT linker options | |
| 40 | ||
| 41 | // Types that can only be specified by numbers, they don't have keywords | |
| 42 | cursor_num, | |
| 43 | icon_num, | |
| 44 | string_num, | |
| 45 | anicursor_num, | |
| 46 | aniicon_num, | |
| 47 | fontdir_num, | |
| 48 | manifest_num, | |
| 49 | ||
| 50 | const map = std.ComptimeStringMapWithEql(Resource, .{ | |
| 51 | .{ "ACCELERATORS", .accelerators }, | |
| 52 | .{ "BITMAP", .bitmap }, | |
| 53 | .{ "CURSOR", .cursor }, | |
| 54 | .{ "DIALOG", .dialog }, | |
| 55 | .{ "DIALOGEX", .dialogex }, | |
| 56 | .{ "DLGINCLUDE", .dlginclude }, | |
| 57 | .{ "DLGINIT", .dlginit }, | |
| 58 | .{ "FONT", .font }, | |
| 59 | .{ "HTML", .html }, | |
| 60 | .{ "ICON", .icon }, | |
| 61 | .{ "MENU", .menu }, | |
| 62 | .{ "MENUEX", .menuex }, | |
| 63 | .{ "MESSAGETABLE", .messagetable }, | |
| 64 | .{ "PLUGPLAY", .plugplay }, | |
| 65 | .{ "RCDATA", .rcdata }, | |
| 66 | .{ "STRINGTABLE", .stringtable }, | |
| 67 | .{ "TOOLBAR", .toolbar }, | |
| 68 | .{ "VERSIONINFO", .versioninfo }, | |
| 69 | .{ "VXD", .vxd }, | |
| 70 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 71 | ||
| 72 | pub fn fromString(bytes: SourceBytes) Resource { | |
| 73 | const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes); | |
| 74 | if (maybe_ordinal) |ordinal| { | |
| 75 | if (ordinal.ordinal >= 256) return .user_defined; | |
| 76 | return fromRT(@enumFromInt(ordinal.ordinal)); | |
| 77 | } | |
| 78 | return map.get(bytes.slice) orelse .user_defined; | |
| 79 | } | |
| 80 | ||
| 81 | // TODO: Some comptime validation that RT <-> Resource conversion is synced? | |
| 82 | pub fn fromRT(rt: res.RT) Resource { | |
| 83 | return switch (rt) { | |
| 84 | .ACCELERATOR => .accelerators, | |
| 85 | .ANICURSOR => .anicursor_num, | |
| 86 | .ANIICON => .aniicon_num, | |
| 87 | .BITMAP => .bitmap, | |
| 88 | .CURSOR => .cursor_num, | |
| 89 | .DIALOG => .dialog, | |
| 90 | .DLGINCLUDE => .dlginclude, | |
| 91 | .DLGINIT => .dlginit, | |
| 92 | .FONT => .font, | |
| 93 | .FONTDIR => .fontdir_num, | |
| 94 | .GROUP_CURSOR => .cursor, | |
| 95 | .GROUP_ICON => .icon, | |
| 96 | .HTML => .html, | |
| 97 | .ICON => .icon_num, | |
| 98 | .MANIFEST => .manifest_num, | |
| 99 | .MENU => .menu, | |
| 100 | .MESSAGETABLE => .messagetable, | |
| 101 | .PLUGPLAY => .plugplay, | |
| 102 | .RCDATA => .rcdata, | |
| 103 | .STRING => .string_num, | |
| 104 | .TOOLBAR => .toolbar, | |
| 105 | .VERSION => .versioninfo, | |
| 106 | .VXD => .vxd, | |
| 107 | _ => .user_defined, | |
| 108 | }; | |
| 109 | } | |
| 110 | ||
| 111 | pub fn canUseRawData(resource: Resource) bool { | |
| 112 | return switch (resource) { | |
| 113 | .user_defined, | |
| 114 | .html, | |
| 115 | .plugplay, // Obsolete | |
| 116 | .rcdata, | |
| 117 | .vxd, // Obsolete | |
| 118 | .manifest_num, | |
| 119 | .dlginit, | |
| 120 | => true, | |
| 121 | else => false, | |
| 122 | }; | |
| 123 | } | |
| 124 | ||
| 125 | pub fn nameForErrorDisplay(resource: Resource) []const u8 { | |
| 126 | return switch (resource) { | |
| 127 | // zig fmt: off | |
| 128 | .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font, | |
| 129 | .html, .icon, .menu, .menuex, .messagetable, .plugplay, .rcdata, .stringtable, | |
| 130 | .toolbar, .versioninfo, .vxd => @tagName(resource), | |
| 131 | // zig fmt: on | |
| 132 | .user_defined => "user-defined", | |
| 133 | .cursor_num => std.fmt.comptimePrint("{d} (cursor)", .{@intFromEnum(res.RT.CURSOR)}), | |
| 134 | .icon_num => std.fmt.comptimePrint("{d} (icon)", .{@intFromEnum(res.RT.ICON)}), | |
| 135 | .string_num => std.fmt.comptimePrint("{d} (string)", .{@intFromEnum(res.RT.STRING)}), | |
| 136 | .anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@intFromEnum(res.RT.ANICURSOR)}), | |
| 137 | .aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@intFromEnum(res.RT.ANIICON)}), | |
| 138 | .fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@intFromEnum(res.RT.FONTDIR)}), | |
| 139 | .manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@intFromEnum(res.RT.MANIFEST)}), | |
| 140 | }; | |
| 141 | } | |
| 142 | }; | |
| 143 | ||
| 144 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable-resource#parameters | |
| 145 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/dialog-resource#parameters | |
| 146 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/dialogex-resource#parameters | |
| 147 | pub const OptionalStatements = enum { | |
| 148 | characteristics, | |
| 149 | language, | |
| 150 | version, | |
| 151 | ||
| 152 | // DIALOG | |
| 153 | caption, | |
| 154 | class, | |
| 155 | exstyle, | |
| 156 | font, | |
| 157 | menu, | |
| 158 | style, | |
| 159 | ||
| 160 | pub const map = std.ComptimeStringMapWithEql(OptionalStatements, .{ | |
| 161 | .{ "CHARACTERISTICS", .characteristics }, | |
| 162 | .{ "LANGUAGE", .language }, | |
| 163 | .{ "VERSION", .version }, | |
| 164 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 165 | ||
| 166 | pub const dialog_map = std.ComptimeStringMapWithEql(OptionalStatements, .{ | |
| 167 | .{ "CAPTION", .caption }, | |
| 168 | .{ "CLASS", .class }, | |
| 169 | .{ "EXSTYLE", .exstyle }, | |
| 170 | .{ "FONT", .font }, | |
| 171 | .{ "MENU", .menu }, | |
| 172 | .{ "STYLE", .style }, | |
| 173 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 174 | }; | |
| 175 | ||
| 176 | pub const Control = enum { | |
| 177 | auto3state, | |
| 178 | autocheckbox, | |
| 179 | autoradiobutton, | |
| 180 | checkbox, | |
| 181 | combobox, | |
| 182 | control, | |
| 183 | ctext, | |
| 184 | defpushbutton, | |
| 185 | edittext, | |
| 186 | hedit, | |
| 187 | iedit, | |
| 188 | groupbox, | |
| 189 | icon, | |
| 190 | listbox, | |
| 191 | ltext, | |
| 192 | pushbox, | |
| 193 | pushbutton, | |
| 194 | radiobutton, | |
| 195 | rtext, | |
| 196 | scrollbar, | |
| 197 | state3, | |
| 198 | userbutton, | |
| 199 | ||
| 200 | pub const map = std.ComptimeStringMapWithEql(Control, .{ | |
| 201 | .{ "AUTO3STATE", .auto3state }, | |
| 202 | .{ "AUTOCHECKBOX", .autocheckbox }, | |
| 203 | .{ "AUTORADIOBUTTON", .autoradiobutton }, | |
| 204 | .{ "CHECKBOX", .checkbox }, | |
| 205 | .{ "COMBOBOX", .combobox }, | |
| 206 | .{ "CONTROL", .control }, | |
| 207 | .{ "CTEXT", .ctext }, | |
| 208 | .{ "DEFPUSHBUTTON", .defpushbutton }, | |
| 209 | .{ "EDITTEXT", .edittext }, | |
| 210 | .{ "HEDIT", .hedit }, | |
| 211 | .{ "IEDIT", .iedit }, | |
| 212 | .{ "GROUPBOX", .groupbox }, | |
| 213 | .{ "ICON", .icon }, | |
| 214 | .{ "LISTBOX", .listbox }, | |
| 215 | .{ "LTEXT", .ltext }, | |
| 216 | .{ "PUSHBOX", .pushbox }, | |
| 217 | .{ "PUSHBUTTON", .pushbutton }, | |
| 218 | .{ "RADIOBUTTON", .radiobutton }, | |
| 219 | .{ "RTEXT", .rtext }, | |
| 220 | .{ "SCROLLBAR", .scrollbar }, | |
| 221 | .{ "STATE3", .state3 }, | |
| 222 | .{ "USERBUTTON", .userbutton }, | |
| 223 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 224 | ||
| 225 | pub fn hasTextParam(control: Control) bool { | |
| 226 | switch (control) { | |
| 227 | .scrollbar, .listbox, .iedit, .hedit, .edittext, .combobox => return false, | |
| 228 | else => return true, | |
| 229 | } | |
| 230 | } | |
| 231 | }; | |
| 232 | ||
| 233 | pub const ControlClass = struct { | |
| 234 | pub const map = std.ComptimeStringMapWithEql(res.ControlClass, .{ | |
| 235 | .{ "BUTTON", .button }, | |
| 236 | .{ "EDIT", .edit }, | |
| 237 | .{ "STATIC", .static }, | |
| 238 | .{ "LISTBOX", .listbox }, | |
| 239 | .{ "SCROLLBAR", .scrollbar }, | |
| 240 | .{ "COMBOBOX", .combobox }, | |
| 241 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 242 | ||
| 243 | /// Like `map.get` but works on WTF16 strings, for use with parsed | |
| 244 | /// string literals ("BUTTON", or even "\x42UTTON") | |
| 245 | pub fn fromWideString(str: []const u16) ?res.ControlClass { | |
| 246 | const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral; | |
| 247 | return if (ascii.eqlIgnoreCaseW(str, utf16Literal("BUTTON"))) | |
| 248 | .button | |
| 249 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("EDIT"))) | |
| 250 | .edit | |
| 251 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("STATIC"))) | |
| 252 | .static | |
| 253 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("LISTBOX"))) | |
| 254 | .listbox | |
| 255 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("SCROLLBAR"))) | |
| 256 | .scrollbar | |
| 257 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("COMBOBOX"))) | |
| 258 | .combobox | |
| 259 | else | |
| 260 | null; | |
| 261 | } | |
| 262 | }; | |
| 263 | ||
| 264 | const ascii = struct { | |
| 265 | /// Compares ASCII values case-insensitively, non-ASCII values are compared directly | |
| 266 | pub fn eqlIgnoreCaseW(a: []const u16, b: []const u16) bool { | |
| 267 | if (a.len != b.len) return false; | |
| 268 | for (a, b) |a_c, b_c| { | |
| 269 | if (a_c < 128) { | |
| 270 | if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false; | |
| 271 | } else { | |
| 272 | if (a_c != b_c) return false; | |
| 273 | } | |
| 274 | } | |
| 275 | return true; | |
| 276 | } | |
| 277 | }; | |
| 278 | ||
| 279 | pub const MenuItem = enum { | |
| 280 | menuitem, | |
| 281 | popup, | |
| 282 | ||
| 283 | pub const map = std.ComptimeStringMapWithEql(MenuItem, .{ | |
| 284 | .{ "MENUITEM", .menuitem }, | |
| 285 | .{ "POPUP", .popup }, | |
| 286 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 287 | ||
| 288 | pub fn isSeparator(bytes: []const u8) bool { | |
| 289 | return std.ascii.eqlIgnoreCase(bytes, "SEPARATOR"); | |
| 290 | } | |
| 291 | ||
| 292 | pub const Option = enum { | |
| 293 | checked, | |
| 294 | grayed, | |
| 295 | help, | |
| 296 | inactive, | |
| 297 | menubarbreak, | |
| 298 | menubreak, | |
| 299 | ||
| 300 | pub const map = std.ComptimeStringMapWithEql(Option, .{ | |
| 301 | .{ "CHECKED", .checked }, | |
| 302 | .{ "GRAYED", .grayed }, | |
| 303 | .{ "HELP", .help }, | |
| 304 | .{ "INACTIVE", .inactive }, | |
| 305 | .{ "MENUBARBREAK", .menubarbreak }, | |
| 306 | .{ "MENUBREAK", .menubreak }, | |
| 307 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 308 | }; | |
| 309 | }; | |
| 310 | ||
| 311 | pub const ToolbarButton = enum { | |
| 312 | button, | |
| 313 | separator, | |
| 314 | ||
| 315 | pub const map = std.ComptimeStringMapWithEql(ToolbarButton, .{ | |
| 316 | .{ "BUTTON", .button }, | |
| 317 | .{ "SEPARATOR", .separator }, | |
| 318 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 319 | }; | |
| 320 | ||
| 321 | pub const VersionInfo = enum { | |
| 322 | file_version, | |
| 323 | product_version, | |
| 324 | file_flags_mask, | |
| 325 | file_flags, | |
| 326 | file_os, | |
| 327 | file_type, | |
| 328 | file_subtype, | |
| 329 | ||
| 330 | pub const map = std.ComptimeStringMapWithEql(VersionInfo, .{ | |
| 331 | .{ "FILEVERSION", .file_version }, | |
| 332 | .{ "PRODUCTVERSION", .product_version }, | |
| 333 | .{ "FILEFLAGSMASK", .file_flags_mask }, | |
| 334 | .{ "FILEFLAGS", .file_flags }, | |
| 335 | .{ "FILEOS", .file_os }, | |
| 336 | .{ "FILETYPE", .file_type }, | |
| 337 | .{ "FILESUBTYPE", .file_subtype }, | |
| 338 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 339 | }; | |
| 340 | ||
| 341 | pub const VersionBlock = enum { | |
| 342 | block, | |
| 343 | value, | |
| 344 | ||
| 345 | pub const map = std.ComptimeStringMapWithEql(VersionBlock, .{ | |
| 346 | .{ "BLOCK", .block }, | |
| 347 | .{ "VALUE", .value }, | |
| 348 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 349 | }; | |
| 350 | ||
| 351 | /// Keywords that are be the first token in a statement and (if so) dictate how the rest | |
| 352 | /// of the statement is parsed. | |
| 353 | pub const TopLevelKeywords = enum { | |
| 354 | language, | |
| 355 | version, | |
| 356 | characteristics, | |
| 357 | stringtable, | |
| 358 | ||
| 359 | pub const map = std.ComptimeStringMapWithEql(TopLevelKeywords, .{ | |
| 360 | .{ "LANGUAGE", .language }, | |
| 361 | .{ "VERSION", .version }, | |
| 362 | .{ "CHARACTERISTICS", .characteristics }, | |
| 363 | .{ "STRINGTABLE", .stringtable }, | |
| 364 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 365 | }; | |
| 366 | ||
| 367 | pub const CommonResourceAttributes = enum { | |
| 368 | preload, | |
| 369 | loadoncall, | |
| 370 | fixed, | |
| 371 | moveable, | |
| 372 | discardable, | |
| 373 | pure, | |
| 374 | impure, | |
| 375 | shared, | |
| 376 | nonshared, | |
| 377 | ||
| 378 | pub const map = std.ComptimeStringMapWithEql(CommonResourceAttributes, .{ | |
| 379 | .{ "PRELOAD", .preload }, | |
| 380 | .{ "LOADONCALL", .loadoncall }, | |
| 381 | .{ "FIXED", .fixed }, | |
| 382 | .{ "MOVEABLE", .moveable }, | |
| 383 | .{ "DISCARDABLE", .discardable }, | |
| 384 | .{ "PURE", .pure }, | |
| 385 | .{ "IMPURE", .impure }, | |
| 386 | .{ "SHARED", .shared }, | |
| 387 | .{ "NONSHARED", .nonshared }, | |
| 388 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 389 | }; | |
| 390 | ||
| 391 | pub const AcceleratorTypeAndOptions = enum { | |
| 392 | virtkey, | |
| 393 | ascii, | |
| 394 | noinvert, | |
| 395 | alt, | |
| 396 | shift, | |
| 397 | control, | |
| 398 | ||
| 399 | pub const map = std.ComptimeStringMapWithEql(AcceleratorTypeAndOptions, .{ | |
| 400 | .{ "VIRTKEY", .virtkey }, | |
| 401 | .{ "ASCII", .ascii }, | |
| 402 | .{ "NOINVERT", .noinvert }, | |
| 403 | .{ "ALT", .alt }, | |
| 404 | .{ "SHIFT", .shift }, | |
| 405 | .{ "CONTROL", .control }, | |
| 406 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 407 | }; |
lib/compiler/resinator/res.zig created+1107| ... | ... | @@ -0,0 +1,1107 @@ |
| 1 | const std = @import("std"); | |
| 2 | const rc = @import("rc.zig"); | |
| 3 | const Resource = rc.Resource; | |
| 4 | const CommonResourceAttributes = rc.CommonResourceAttributes; | |
| 5 | const Allocator = std.mem.Allocator; | |
| 6 | const windows1252 = @import("windows1252.zig"); | |
| 7 | const CodePage = @import("code_pages.zig").CodePage; | |
| 8 | const literals = @import("literals.zig"); | |
| 9 | const SourceBytes = literals.SourceBytes; | |
| 10 | const Codepoint = @import("code_pages.zig").Codepoint; | |
| 11 | const lang = @import("lang.zig"); | |
| 12 | const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit; | |
| 13 | ||
| 14 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types | |
| 15 | pub const RT = enum(u8) { | |
| 16 | ACCELERATOR = 9, | |
| 17 | ANICURSOR = 21, | |
| 18 | ANIICON = 22, | |
| 19 | BITMAP = 2, | |
| 20 | CURSOR = 1, | |
| 21 | DIALOG = 5, | |
| 22 | DLGINCLUDE = 17, | |
| 23 | DLGINIT = 240, | |
| 24 | FONT = 8, | |
| 25 | FONTDIR = 7, | |
| 26 | GROUP_CURSOR = 1 + 11, // CURSOR + 11 | |
| 27 | GROUP_ICON = 3 + 11, // ICON + 11 | |
| 28 | HTML = 23, | |
| 29 | ICON = 3, | |
| 30 | MANIFEST = 24, | |
| 31 | MENU = 4, | |
| 32 | MESSAGETABLE = 11, | |
| 33 | PLUGPLAY = 19, | |
| 34 | RCDATA = 10, | |
| 35 | STRING = 6, | |
| 36 | TOOLBAR = 241, | |
| 37 | VERSION = 16, | |
| 38 | VXD = 20, | |
| 39 | _, | |
| 40 | ||
| 41 | /// Returns null if the resource type is user-defined | |
| 42 | /// Asserts that the resource is not `stringtable` | |
| 43 | pub fn fromResource(resource: Resource) ?RT { | |
| 44 | return switch (resource) { | |
| 45 | .accelerators => .ACCELERATOR, | |
| 46 | .bitmap => .BITMAP, | |
| 47 | .cursor => .GROUP_CURSOR, | |
| 48 | .dialog => .DIALOG, | |
| 49 | .dialogex => .DIALOG, | |
| 50 | .dlginclude => .DLGINCLUDE, | |
| 51 | .dlginit => .DLGINIT, | |
| 52 | .font => .FONT, | |
| 53 | .html => .HTML, | |
| 54 | .icon => .GROUP_ICON, | |
| 55 | .menu => .MENU, | |
| 56 | .menuex => .MENU, | |
| 57 | .messagetable => .MESSAGETABLE, | |
| 58 | .plugplay => .PLUGPLAY, | |
| 59 | .rcdata => .RCDATA, | |
| 60 | .stringtable => unreachable, | |
| 61 | .toolbar => .TOOLBAR, | |
| 62 | .user_defined => null, | |
| 63 | .versioninfo => .VERSION, | |
| 64 | .vxd => .VXD, | |
| 65 | ||
| 66 | .cursor_num => .CURSOR, | |
| 67 | .icon_num => .ICON, | |
| 68 | .string_num => .STRING, | |
| 69 | .anicursor_num => .ANICURSOR, | |
| 70 | .aniicon_num => .ANIICON, | |
| 71 | .fontdir_num => .FONTDIR, | |
| 72 | .manifest_num => .MANIFEST, | |
| 73 | }; | |
| 74 | } | |
| 75 | }; | |
| 76 | ||
| 77 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/common-resource-attributes | |
| 78 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader | |
| 79 | pub const MemoryFlags = packed struct(u16) { | |
| 80 | value: u16, | |
| 81 | ||
| 82 | pub const MOVEABLE: u16 = 0x10; | |
| 83 | // TODO: SHARED and PURE seem to be the same thing? Testing seems to confirm this but | |
| 84 | // would like to find mention of it somewhere. | |
| 85 | pub const SHARED: u16 = 0x20; | |
| 86 | pub const PURE: u16 = 0x20; | |
| 87 | pub const PRELOAD: u16 = 0x40; | |
| 88 | pub const DISCARDABLE: u16 = 0x1000; | |
| 89 | ||
| 90 | /// Note: The defaults can have combinations that are not possible to specify within | |
| 91 | /// an .rc file, as the .rc attributes imply other values (i.e. specifying | |
| 92 | /// DISCARDABLE always implies MOVEABLE and PURE/SHARED, and yet RT_ICON | |
| 93 | /// has a default of only MOVEABLE | DISCARDABLE). | |
| 94 | pub fn defaults(predefined_resource_type: ?RT) MemoryFlags { | |
| 95 | if (predefined_resource_type == null) { | |
| 96 | return MemoryFlags{ .value = MOVEABLE | SHARED }; | |
| 97 | } else { | |
| 98 | return switch (predefined_resource_type.?) { | |
| 99 | // zig fmt: off | |
| 100 | .RCDATA, .BITMAP, .HTML, .MANIFEST, | |
| 101 | .ACCELERATOR, .VERSION, .MESSAGETABLE, | |
| 102 | .DLGINIT, .TOOLBAR, .PLUGPLAY, | |
| 103 | .VXD, => MemoryFlags{ .value = MOVEABLE | SHARED }, | |
| 104 | ||
| 105 | .GROUP_ICON, .GROUP_CURSOR, | |
| 106 | .STRING, .FONT, .DIALOG, .MENU, | |
| 107 | .DLGINCLUDE, => MemoryFlags{ .value = MOVEABLE | SHARED | DISCARDABLE }, | |
| 108 | ||
| 109 | .ICON, .CURSOR, .ANIICON, .ANICURSOR => MemoryFlags{ .value = MOVEABLE | DISCARDABLE }, | |
| 110 | .FONTDIR => MemoryFlags{ .value = MOVEABLE | PRELOAD }, | |
| 111 | // zig fmt: on | |
| 112 | // Same as predefined_resource_type == null | |
| 113 | _ => return MemoryFlags{ .value = MOVEABLE | SHARED }, | |
| 114 | }; | |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | pub fn set(self: *MemoryFlags, attribute: CommonResourceAttributes) void { | |
| 119 | switch (attribute) { | |
| 120 | .preload => self.value |= PRELOAD, | |
| 121 | .loadoncall => self.value &= ~PRELOAD, | |
| 122 | .moveable => self.value |= MOVEABLE, | |
| 123 | .fixed => self.value &= ~(MOVEABLE | DISCARDABLE), | |
| 124 | .shared => self.value |= SHARED, | |
| 125 | .nonshared => self.value &= ~(SHARED | DISCARDABLE), | |
| 126 | .pure => self.value |= PURE, | |
| 127 | .impure => self.value &= ~(PURE | DISCARDABLE), | |
| 128 | .discardable => self.value |= DISCARDABLE | MOVEABLE | PURE, | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | pub fn setGroup(self: *MemoryFlags, attribute: CommonResourceAttributes, implied_shared_or_pure: bool) void { | |
| 133 | switch (attribute) { | |
| 134 | .preload => { | |
| 135 | self.value |= PRELOAD; | |
| 136 | if (implied_shared_or_pure) self.value &= ~SHARED; | |
| 137 | }, | |
| 138 | .loadoncall => { | |
| 139 | self.value &= ~PRELOAD; | |
| 140 | if (implied_shared_or_pure) self.value |= SHARED; | |
| 141 | }, | |
| 142 | else => self.set(attribute), | |
| 143 | } | |
| 144 | } | |
| 145 | }; | |
| 146 | ||
| 147 | /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers | |
| 148 | pub const Language = packed struct(u16) { | |
| 149 | // Note: This is the default no matter what locale the current system is set to, | |
| 150 | // e.g. even if the system's locale is en-GB, en-US will still be the | |
| 151 | // default language for resources in the Win32 rc compiler. | |
| 152 | primary_language_id: u10 = lang.LANG_ENGLISH, | |
| 153 | sublanguage_id: u6 = lang.SUBLANG_ENGLISH_US, | |
| 154 | ||
| 155 | /// Default language ID as a u16 | |
| 156 | pub const default: u16 = (Language{}).asInt(); | |
| 157 | ||
| 158 | pub fn fromInt(int: u16) Language { | |
| 159 | return @bitCast(int); | |
| 160 | } | |
| 161 | ||
| 162 | pub fn asInt(self: Language) u16 { | |
| 163 | return @bitCast(self); | |
| 164 | } | |
| 165 | }; | |
| 166 | ||
| 167 | /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks | |
| 168 | pub const ControlClass = enum(u16) { | |
| 169 | button = 0x80, | |
| 170 | edit = 0x81, | |
| 171 | static = 0x82, | |
| 172 | listbox = 0x83, | |
| 173 | scrollbar = 0x84, | |
| 174 | combobox = 0x85, | |
| 175 | ||
| 176 | pub fn fromControl(control: rc.Control) ?ControlClass { | |
| 177 | return switch (control) { | |
| 178 | // zig fmt: off | |
| 179 | .auto3state, .autocheckbox, .autoradiobutton, | |
| 180 | .checkbox, .defpushbutton, .groupbox, .pushbox, | |
| 181 | .pushbutton, .radiobutton, .state3, .userbutton => .button, | |
| 182 | // zig fmt: on | |
| 183 | .combobox => .combobox, | |
| 184 | .control => null, | |
| 185 | .ctext, .icon, .ltext, .rtext => .static, | |
| 186 | .edittext, .hedit, .iedit => .edit, | |
| 187 | .listbox => .listbox, | |
| 188 | .scrollbar => .scrollbar, | |
| 189 | }; | |
| 190 | } | |
| 191 | ||
| 192 | pub fn getImpliedStyle(control: rc.Control) u32 { | |
| 193 | var style = WS.CHILD | WS.VISIBLE; | |
| 194 | switch (control) { | |
| 195 | .auto3state => style |= BS.AUTO3STATE | WS.TABSTOP, | |
| 196 | .autocheckbox => style |= BS.AUTOCHECKBOX | WS.TABSTOP, | |
| 197 | .autoradiobutton => style |= BS.AUTORADIOBUTTON, | |
| 198 | .checkbox => style |= BS.CHECKBOX | WS.TABSTOP, | |
| 199 | .combobox => {}, | |
| 200 | .control => {}, | |
| 201 | .ctext => style |= SS.CENTER | WS.GROUP, | |
| 202 | .defpushbutton => style |= BS.DEFPUSHBUTTON | WS.TABSTOP, | |
| 203 | .edittext, .hedit, .iedit => style |= WS.TABSTOP | WS.BORDER, | |
| 204 | .groupbox => style |= BS.GROUPBOX, | |
| 205 | .icon => style |= SS.ICON, | |
| 206 | .listbox => style |= LBS.NOTIFY | WS.BORDER, | |
| 207 | .ltext => style |= WS.GROUP, | |
| 208 | .pushbox => style |= BS.PUSHBOX | WS.TABSTOP, | |
| 209 | .pushbutton => style |= WS.TABSTOP, | |
| 210 | .radiobutton => style |= BS.RADIOBUTTON, | |
| 211 | .rtext => style |= SS.RIGHT | WS.GROUP, | |
| 212 | .scrollbar => {}, | |
| 213 | .state3 => style |= BS.@"3STATE" | WS.TABSTOP, | |
| 214 | .userbutton => style |= BS.USERBUTTON | WS.TABSTOP, | |
| 215 | } | |
| 216 | return style; | |
| 217 | } | |
| 218 | }; | |
| 219 | ||
| 220 | pub const NameOrOrdinal = union(enum) { | |
| 221 | // 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 | ||
| 428 | fn 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 | ||
| 441 | test "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 | ||
| 536 | test "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 | |
| 564 | pub 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 | ||
| 604 | const 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 | ||
| 617 | pub 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""). | |
| 620 | pub 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 | ||
| 698 | test "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 | ||
| 878 | pub 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 | ||
| 916 | test "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 | |
| 936 | pub 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 | ||
| 982 | test "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 | ||
| 990 | pub const VersionNode = struct { | |
| 991 | pub const type_string: u16 = 1; | |
| 992 | pub const type_binary: u16 = 0; | |
| 993 | }; | |
| 994 | ||
| 995 | pub 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 | |
| 1024 | pub 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 | |
| 1036 | pub 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 | |
| 1071 | pub 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 | |
| 1077 | pub 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 | |
| 1096 | pub 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 | |
| 1105 | pub const LBS = struct { | |
| 1106 | pub const NOTIFY: u32 = 0x0001; | |
| 1107 | }; |
lib/compiler/resinator/source_mapping.zig created+831| ... | ... | @@ -0,0 +1,831 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Allocator = std.mem.Allocator; | |
| 3 | const utils = @import("utils.zig"); | |
| 4 | const UncheckedSliceWriter = utils.UncheckedSliceWriter; | |
| 5 | ||
| 6 | pub const ParseLineCommandsResult = struct { | |
| 7 | result: []u8, | |
| 8 | mappings: SourceMappings, | |
| 9 | }; | |
| 10 | ||
| 11 | const CurrentMapping = struct { | |
| 12 | line_num: usize = 1, | |
| 13 | filename: std.ArrayListUnmanaged(u8) = .{}, | |
| 14 | pending: bool = true, | |
| 15 | ignore_contents: bool = false, | |
| 16 | }; | |
| 17 | ||
| 18 | pub 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. | |
| 41 | pub 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 | |
| 198 | pub 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 | ||
| 205 | pub 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 | |
| 215 | pub 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 | ||
| 246 | pub 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. | |
| 264 | fn 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 | ||
| 395 | fn 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 | ||
| 402 | test 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 | ||
| 416 | pub 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 | ||
| 606 | test "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 | |
| 631 | pub 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 | ||
| 675 | const ExpectedSourceSpan = struct { | |
| 676 | start_line: usize, | |
| 677 | end_line: usize, | |
| 678 | filename: []const u8, | |
| 679 | }; | |
| 680 | ||
| 681 | fn 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 | ||
| 711 | fn 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 | ||
| 723 | test "basic" { | |
| 724 | try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{ | |
| 725 | .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" }, | |
| 726 | }, "#line 1 \"blah.rc\"", .{}); | |
| 727 | } | |
| 728 | ||
| 729 | test "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 | ||
| 740 | test "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 | ||
| 746 | test "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 | ||
| 775 | test "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 | ||
| 788 | test "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 | ||
| 801 | test "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 | ||
| 808 | test "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+124| ... | ... | @@ -0,0 +1,124 @@ |
| 1 | const std = @import("std"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | ||
| 4 | /// Like std.io.FixedBufferStream but does no bounds checking | |
| 5 | pub const UncheckedSliceWriter = struct { | |
| 6 | const Self = @This(); | |
| 7 | ||
| 8 | pos: usize = 0, | |
| 9 | slice: []u8, | |
| 10 | ||
| 11 | pub fn write(self: *Self, char: u8) void { | |
| 12 | self.slice[self.pos] = char; | |
| 13 | self.pos += 1; | |
| 14 | } | |
| 15 | ||
| 16 | pub fn writeSlice(self: *Self, slice: []const u8) void { | |
| 17 | for (slice) |c| { | |
| 18 | self.write(c); | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | pub fn getWritten(self: Self) []u8 { | |
| 23 | return self.slice[0..self.pos]; | |
| 24 | } | |
| 25 | }; | |
| 26 | ||
| 27 | /// Cross-platform 'std.fs.Dir.openFile' wrapper that will always return IsDir if | |
| 28 | /// a directory is attempted to be opened. | |
| 29 | /// TODO: Remove once https://github.com/ziglang/zig/issues/5732 is addressed. | |
| 30 | pub fn openFileNotDir(cwd: std.fs.Dir, path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File { | |
| 31 | const file = try cwd.openFile(path, flags); | |
| 32 | errdefer file.close(); | |
| 33 | // https://github.com/ziglang/zig/issues/5732 | |
| 34 | if (builtin.os.tag != .windows) { | |
| 35 | const stat = try file.stat(); | |
| 36 | ||
| 37 | if (stat.kind == .directory) | |
| 38 | return error.IsDir; | |
| 39 | } | |
| 40 | return file; | |
| 41 | } | |
| 42 | ||
| 43 | /// Emulates the Windows implementation of `iswdigit`, but only returns true | |
| 44 | /// for the non-ASCII digits that `iswdigit` on Windows would return true for. | |
| 45 | pub fn isNonAsciiDigit(c: u21) bool { | |
| 46 | return switch (c) { | |
| 47 | '²', | |
| 48 | '³', | |
| 49 | '¹', | |
| 50 | '\u{660}'...'\u{669}', | |
| 51 | '\u{6F0}'...'\u{6F9}', | |
| 52 | '\u{7C0}'...'\u{7C9}', | |
| 53 | '\u{966}'...'\u{96F}', | |
| 54 | '\u{9E6}'...'\u{9EF}', | |
| 55 | '\u{A66}'...'\u{A6F}', | |
| 56 | '\u{AE6}'...'\u{AEF}', | |
| 57 | '\u{B66}'...'\u{B6F}', | |
| 58 | '\u{BE6}'...'\u{BEF}', | |
| 59 | '\u{C66}'...'\u{C6F}', | |
| 60 | '\u{CE6}'...'\u{CEF}', | |
| 61 | '\u{D66}'...'\u{D6F}', | |
| 62 | '\u{E50}'...'\u{E59}', | |
| 63 | '\u{ED0}'...'\u{ED9}', | |
| 64 | '\u{F20}'...'\u{F29}', | |
| 65 | '\u{1040}'...'\u{1049}', | |
| 66 | '\u{1090}'...'\u{1099}', | |
| 67 | '\u{17E0}'...'\u{17E9}', | |
| 68 | '\u{1810}'...'\u{1819}', | |
| 69 | '\u{1946}'...'\u{194F}', | |
| 70 | '\u{19D0}'...'\u{19D9}', | |
| 71 | '\u{1B50}'...'\u{1B59}', | |
| 72 | '\u{1BB0}'...'\u{1BB9}', | |
| 73 | '\u{1C40}'...'\u{1C49}', | |
| 74 | '\u{1C50}'...'\u{1C59}', | |
| 75 | '\u{A620}'...'\u{A629}', | |
| 76 | '\u{A8D0}'...'\u{A8D9}', | |
| 77 | '\u{A900}'...'\u{A909}', | |
| 78 | '\u{AA50}'...'\u{AA59}', | |
| 79 | '\u{FF10}'...'\u{FF19}', | |
| 80 | => true, | |
| 81 | else => false, | |
| 82 | }; | |
| 83 | } | |
| 84 | ||
| 85 | pub const ErrorMessageType = enum { err, warning, note }; | |
| 86 | ||
| 87 | /// Used for generic colored errors/warnings/notes, more context-specific error messages | |
| 88 | /// are handled elsewhere. | |
| 89 | pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type: ErrorMessageType, comptime format: []const u8, args: anytype) !void { | |
| 90 | switch (msg_type) { | |
| 91 | .err => { | |
| 92 | try config.setColor(writer, .bold); | |
| 93 | try config.setColor(writer, .red); | |
| 94 | try writer.writeAll("error: "); | |
| 95 | }, | |
| 96 | .warning => { | |
| 97 | try config.setColor(writer, .bold); | |
| 98 | try config.setColor(writer, .yellow); | |
| 99 | try writer.writeAll("warning: "); | |
| 100 | }, | |
| 101 | .note => { | |
| 102 | try config.setColor(writer, .reset); | |
| 103 | try config.setColor(writer, .cyan); | |
| 104 | try writer.writeAll("note: "); | |
| 105 | }, | |
| 106 | } | |
| 107 | try config.setColor(writer, .reset); | |
| 108 | if (msg_type == .err) { | |
| 109 | try config.setColor(writer, .bold); | |
| 110 | } | |
| 111 | try writer.print(format, args); | |
| 112 | try writer.writeByte('\n'); | |
| 113 | try config.setColor(writer, .reset); | |
| 114 | } | |
| 115 | ||
| 116 | pub fn isLineEndingPair(first: u8, second: u8) bool { | |
| 117 | if (first != '\r' and first != '\n') return false; | |
| 118 | if (second != '\r' and second != '\n') return false; | |
| 119 | ||
| 120 | // can't be \n\n or \r\r | |
| 121 | if (first == second) return false; | |
| 122 | ||
| 123 | return true; | |
| 124 | } |
lib/compiler/resinator/windows1252.zig created+588| ... | ... | @@ -0,0 +1,588 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | pub fn windows1252ToUtf8Stream(writer: anytype, reader: anytype) !usize { | |
| 4 | var bytes_written: usize = 0; | |
| 5 | var utf8_buf: [3]u8 = undefined; | |
| 6 | while (true) { | |
| 7 | const c = reader.readByte() catch |err| switch (err) { | |
| 8 | error.EndOfStream => return bytes_written, | |
| 9 | else => |e| return e, | |
| 10 | }; | |
| 11 | const codepoint = toCodepoint(c); | |
| 12 | if (codepoint <= 0x7F) { | |
| 13 | try writer.writeByte(c); | |
| 14 | bytes_written += 1; | |
| 15 | } else { | |
| 16 | const utf8_len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch unreachable; | |
| 17 | try writer.writeAll(utf8_buf[0..utf8_len]); | |
| 18 | bytes_written += utf8_len; | |
| 19 | } | |
| 20 | } | |
| 21 | } | |
| 22 | ||
| 23 | /// Returns the number of code units written to the writer | |
| 24 | pub fn windows1252ToUtf16AllocZ(allocator: std.mem.Allocator, win1252_str: []const u8) ![:0]u16 { | |
| 25 | // Guaranteed to need exactly the same number of code units as Windows-1252 bytes | |
| 26 | var utf16_slice = try allocator.allocSentinel(u16, win1252_str.len, 0); | |
| 27 | errdefer allocator.free(utf16_slice); | |
| 28 | for (win1252_str, 0..) |c, i| { | |
| 29 | utf16_slice[i] = toCodepoint(c); | |
| 30 | } | |
| 31 | return utf16_slice; | |
| 32 | } | |
| 33 | ||
| 34 | /// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt | |
| 35 | pub fn toCodepoint(c: u8) u16 { | |
| 36 | return switch (c) { | |
| 37 | 0x80 => 0x20ac, // Euro Sign | |
| 38 | 0x82 => 0x201a, // Single Low-9 Quotation Mark | |
| 39 | 0x83 => 0x0192, // Latin Small Letter F With Hook | |
| 40 | 0x84 => 0x201e, // Double Low-9 Quotation Mark | |
| 41 | 0x85 => 0x2026, // Horizontal Ellipsis | |
| 42 | 0x86 => 0x2020, // Dagger | |
| 43 | 0x87 => 0x2021, // Double Dagger | |
| 44 | 0x88 => 0x02c6, // Modifier Letter Circumflex Accent | |
| 45 | 0x89 => 0x2030, // Per Mille Sign | |
| 46 | 0x8a => 0x0160, // Latin Capital Letter S With Caron | |
| 47 | 0x8b => 0x2039, // Single Left-Pointing Angle Quotation Mark | |
| 48 | 0x8c => 0x0152, // Latin Capital Ligature Oe | |
| 49 | 0x8e => 0x017d, // Latin Capital Letter Z With Caron | |
| 50 | 0x91 => 0x2018, // Left Single Quotation Mark | |
| 51 | 0x92 => 0x2019, // Right Single Quotation Mark | |
| 52 | 0x93 => 0x201c, // Left Double Quotation Mark | |
| 53 | 0x94 => 0x201d, // Right Double Quotation Mark | |
| 54 | 0x95 => 0x2022, // Bullet | |
| 55 | 0x96 => 0x2013, // En Dash | |
| 56 | 0x97 => 0x2014, // Em Dash | |
| 57 | 0x98 => 0x02dc, // Small Tilde | |
| 58 | 0x99 => 0x2122, // Trade Mark Sign | |
| 59 | 0x9a => 0x0161, // Latin Small Letter S With Caron | |
| 60 | 0x9b => 0x203a, // Single Right-Pointing Angle Quotation Mark | |
| 61 | 0x9c => 0x0153, // Latin Small Ligature Oe | |
| 62 | 0x9e => 0x017e, // Latin Small Letter Z With Caron | |
| 63 | 0x9f => 0x0178, // Latin Capital Letter Y With Diaeresis | |
| 64 | else => c, | |
| 65 | }; | |
| 66 | } | |
| 67 | ||
| 68 | /// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt | |
| 69 | /// Plus some mappings found empirically by iterating all codepoints: | |
| 70 | /// 0x2007 => 0xA0, // Figure Space | |
| 71 | /// 0x2008 => ' ', // Punctuation Space | |
| 72 | /// 0x2009 => ' ', // Thin Space | |
| 73 | /// 0x200A => ' ', // Hair Space | |
| 74 | /// 0x2012 => '-', // Figure Dash | |
| 75 | /// 0x2015 => '-', // Horizontal Bar | |
| 76 | /// 0x201B => '\'', // Single High-reversed-9 Quotation Mark | |
| 77 | /// 0x201F => '"', // Double High-reversed-9 Quotation Mark | |
| 78 | /// 0x202F => 0xA0, // Narrow No-Break Space | |
| 79 | /// 0x2033 => '"', // Double Prime | |
| 80 | /// 0x2036 => '"', // Reversed Double Prime | |
| 81 | pub fn bestFitFromCodepoint(codepoint: u21) ?u8 { | |
| 82 | return switch (codepoint) { | |
| 83 | 0x00...0x7F, | |
| 84 | 0x81, | |
| 85 | 0x8D, | |
| 86 | 0x8F, | |
| 87 | 0x90, | |
| 88 | 0x9D, | |
| 89 | 0xA0...0xFF, | |
| 90 | => @intCast(codepoint), | |
| 91 | 0x0100 => 0x41, // Latin Capital Letter A With Macron | |
| 92 | 0x0101 => 0x61, // Latin Small Letter A With Macron | |
| 93 | 0x0102 => 0x41, // Latin Capital Letter A With Breve | |
| 94 | 0x0103 => 0x61, // Latin Small Letter A With Breve | |
| 95 | 0x0104 => 0x41, // Latin Capital Letter A With Ogonek | |
| 96 | 0x0105 => 0x61, // Latin Small Letter A With Ogonek | |
| 97 | 0x0106 => 0x43, // Latin Capital Letter C With Acute | |
| 98 | 0x0107 => 0x63, // Latin Small Letter C With Acute | |
| 99 | 0x0108 => 0x43, // Latin Capital Letter C With Circumflex | |
| 100 | 0x0109 => 0x63, // Latin Small Letter C With Circumflex | |
| 101 | 0x010a => 0x43, // Latin Capital Letter C With Dot Above | |
| 102 | 0x010b => 0x63, // Latin Small Letter C With Dot Above | |
| 103 | 0x010c => 0x43, // Latin Capital Letter C With Caron | |
| 104 | 0x010d => 0x63, // Latin Small Letter C With Caron | |
| 105 | 0x010e => 0x44, // Latin Capital Letter D With Caron | |
| 106 | 0x010f => 0x64, // Latin Small Letter D With Caron | |
| 107 | 0x0110 => 0xd0, // Latin Capital Letter D With Stroke | |
| 108 | 0x0111 => 0x64, // Latin Small Letter D With Stroke | |
| 109 | 0x0112 => 0x45, // Latin Capital Letter E With Macron | |
| 110 | 0x0113 => 0x65, // Latin Small Letter E With Macron | |
| 111 | 0x0114 => 0x45, // Latin Capital Letter E With Breve | |
| 112 | 0x0115 => 0x65, // Latin Small Letter E With Breve | |
| 113 | 0x0116 => 0x45, // Latin Capital Letter E With Dot Above | |
| 114 | 0x0117 => 0x65, // Latin Small Letter E With Dot Above | |
| 115 | 0x0118 => 0x45, // Latin Capital Letter E With Ogonek | |
| 116 | 0x0119 => 0x65, // Latin Small Letter E With Ogonek | |
| 117 | 0x011a => 0x45, // Latin Capital Letter E With Caron | |
| 118 | 0x011b => 0x65, // Latin Small Letter E With Caron | |
| 119 | 0x011c => 0x47, // Latin Capital Letter G With Circumflex | |
| 120 | 0x011d => 0x67, // Latin Small Letter G With Circumflex | |
| 121 | 0x011e => 0x47, // Latin Capital Letter G With Breve | |
| 122 | 0x011f => 0x67, // Latin Small Letter G With Breve | |
| 123 | 0x0120 => 0x47, // Latin Capital Letter G With Dot Above | |
| 124 | 0x0121 => 0x67, // Latin Small Letter G With Dot Above | |
| 125 | 0x0122 => 0x47, // Latin Capital Letter G With Cedilla | |
| 126 | 0x0123 => 0x67, // Latin Small Letter G With Cedilla | |
| 127 | 0x0124 => 0x48, // Latin Capital Letter H With Circumflex | |
| 128 | 0x0125 => 0x68, // Latin Small Letter H With Circumflex | |
| 129 | 0x0126 => 0x48, // Latin Capital Letter H With Stroke | |
| 130 | 0x0127 => 0x68, // Latin Small Letter H With Stroke | |
| 131 | 0x0128 => 0x49, // Latin Capital Letter I With Tilde | |
| 132 | 0x0129 => 0x69, // Latin Small Letter I With Tilde | |
| 133 | 0x012a => 0x49, // Latin Capital Letter I With Macron | |
| 134 | 0x012b => 0x69, // Latin Small Letter I With Macron | |
| 135 | 0x012c => 0x49, // Latin Capital Letter I With Breve | |
| 136 | 0x012d => 0x69, // Latin Small Letter I With Breve | |
| 137 | 0x012e => 0x49, // Latin Capital Letter I With Ogonek | |
| 138 | 0x012f => 0x69, // Latin Small Letter I With Ogonek | |
| 139 | 0x0130 => 0x49, // Latin Capital Letter I With Dot Above | |
| 140 | 0x0131 => 0x69, // Latin Small Letter Dotless I | |
| 141 | 0x0134 => 0x4a, // Latin Capital Letter J With Circumflex | |
| 142 | 0x0135 => 0x6a, // Latin Small Letter J With Circumflex | |
| 143 | 0x0136 => 0x4b, // Latin Capital Letter K With Cedilla | |
| 144 | 0x0137 => 0x6b, // Latin Small Letter K With Cedilla | |
| 145 | 0x0139 => 0x4c, // Latin Capital Letter L With Acute | |
| 146 | 0x013a => 0x6c, // Latin Small Letter L With Acute | |
| 147 | 0x013b => 0x4c, // Latin Capital Letter L With Cedilla | |
| 148 | 0x013c => 0x6c, // Latin Small Letter L With Cedilla | |
| 149 | 0x013d => 0x4c, // Latin Capital Letter L With Caron | |
| 150 | 0x013e => 0x6c, // Latin Small Letter L With Caron | |
| 151 | 0x0141 => 0x4c, // Latin Capital Letter L With Stroke | |
| 152 | 0x0142 => 0x6c, // Latin Small Letter L With Stroke | |
| 153 | 0x0143 => 0x4e, // Latin Capital Letter N With Acute | |
| 154 | 0x0144 => 0x6e, // Latin Small Letter N With Acute | |
| 155 | 0x0145 => 0x4e, // Latin Capital Letter N With Cedilla | |
| 156 | 0x0146 => 0x6e, // Latin Small Letter N With Cedilla | |
| 157 | 0x0147 => 0x4e, // Latin Capital Letter N With Caron | |
| 158 | 0x0148 => 0x6e, // Latin Small Letter N With Caron | |
| 159 | 0x014c => 0x4f, // Latin Capital Letter O With Macron | |
| 160 | 0x014d => 0x6f, // Latin Small Letter O With Macron | |
| 161 | 0x014e => 0x4f, // Latin Capital Letter O With Breve | |
| 162 | 0x014f => 0x6f, // Latin Small Letter O With Breve | |
| 163 | 0x0150 => 0x4f, // Latin Capital Letter O With Double Acute | |
| 164 | 0x0151 => 0x6f, // Latin Small Letter O With Double Acute | |
| 165 | 0x0152 => 0x8c, // Latin Capital Ligature Oe | |
| 166 | 0x0153 => 0x9c, // Latin Small Ligature Oe | |
| 167 | 0x0154 => 0x52, // Latin Capital Letter R With Acute | |
| 168 | 0x0155 => 0x72, // Latin Small Letter R With Acute | |
| 169 | 0x0156 => 0x52, // Latin Capital Letter R With Cedilla | |
| 170 | 0x0157 => 0x72, // Latin Small Letter R With Cedilla | |
| 171 | 0x0158 => 0x52, // Latin Capital Letter R With Caron | |
| 172 | 0x0159 => 0x72, // Latin Small Letter R With Caron | |
| 173 | 0x015a => 0x53, // Latin Capital Letter S With Acute | |
| 174 | 0x015b => 0x73, // Latin Small Letter S With Acute | |
| 175 | 0x015c => 0x53, // Latin Capital Letter S With Circumflex | |
| 176 | 0x015d => 0x73, // Latin Small Letter S With Circumflex | |
| 177 | 0x015e => 0x53, // Latin Capital Letter S With Cedilla | |
| 178 | 0x015f => 0x73, // Latin Small Letter S With Cedilla | |
| 179 | 0x0160 => 0x8a, // Latin Capital Letter S With Caron | |
| 180 | 0x0161 => 0x9a, // Latin Small Letter S With Caron | |
| 181 | 0x0162 => 0x54, // Latin Capital Letter T With Cedilla | |
| 182 | 0x0163 => 0x74, // Latin Small Letter T With Cedilla | |
| 183 | 0x0164 => 0x54, // Latin Capital Letter T With Caron | |
| 184 | 0x0165 => 0x74, // Latin Small Letter T With Caron | |
| 185 | 0x0166 => 0x54, // Latin Capital Letter T With Stroke | |
| 186 | 0x0167 => 0x74, // Latin Small Letter T With Stroke | |
| 187 | 0x0168 => 0x55, // Latin Capital Letter U With Tilde | |
| 188 | 0x0169 => 0x75, // Latin Small Letter U With Tilde | |
| 189 | 0x016a => 0x55, // Latin Capital Letter U With Macron | |
| 190 | 0x016b => 0x75, // Latin Small Letter U With Macron | |
| 191 | 0x016c => 0x55, // Latin Capital Letter U With Breve | |
| 192 | 0x016d => 0x75, // Latin Small Letter U With Breve | |
| 193 | 0x016e => 0x55, // Latin Capital Letter U With Ring Above | |
| 194 | 0x016f => 0x75, // Latin Small Letter U With Ring Above | |
| 195 | 0x0170 => 0x55, // Latin Capital Letter U With Double Acute | |
| 196 | 0x0171 => 0x75, // Latin Small Letter U With Double Acute | |
| 197 | 0x0172 => 0x55, // Latin Capital Letter U With Ogonek | |
| 198 | 0x0173 => 0x75, // Latin Small Letter U With Ogonek | |
| 199 | 0x0174 => 0x57, // Latin Capital Letter W With Circumflex | |
| 200 | 0x0175 => 0x77, // Latin Small Letter W With Circumflex | |
| 201 | 0x0176 => 0x59, // Latin Capital Letter Y With Circumflex | |
| 202 | 0x0177 => 0x79, // Latin Small Letter Y With Circumflex | |
| 203 | 0x0178 => 0x9f, // Latin Capital Letter Y With Diaeresis | |
| 204 | 0x0179 => 0x5a, // Latin Capital Letter Z With Acute | |
| 205 | 0x017a => 0x7a, // Latin Small Letter Z With Acute | |
| 206 | 0x017b => 0x5a, // Latin Capital Letter Z With Dot Above | |
| 207 | 0x017c => 0x7a, // Latin Small Letter Z With Dot Above | |
| 208 | 0x017d => 0x8e, // Latin Capital Letter Z With Caron | |
| 209 | 0x017e => 0x9e, // Latin Small Letter Z With Caron | |
| 210 | 0x0180 => 0x62, // Latin Small Letter B With Stroke | |
| 211 | 0x0189 => 0xd0, // Latin Capital Letter African D | |
| 212 | 0x0191 => 0x83, // Latin Capital Letter F With Hook | |
| 213 | 0x0192 => 0x83, // Latin Small Letter F With Hook | |
| 214 | 0x0197 => 0x49, // Latin Capital Letter I With Stroke | |
| 215 | 0x019a => 0x6c, // Latin Small Letter L With Bar | |
| 216 | 0x019f => 0x4f, // Latin Capital Letter O With Middle Tilde | |
| 217 | 0x01a0 => 0x4f, // Latin Capital Letter O With Horn | |
| 218 | 0x01a1 => 0x6f, // Latin Small Letter O With Horn | |
| 219 | 0x01ab => 0x74, // Latin Small Letter T With Palatal Hook | |
| 220 | 0x01ae => 0x54, // Latin Capital Letter T With Retroflex Hook | |
| 221 | 0x01af => 0x55, // Latin Capital Letter U With Horn | |
| 222 | 0x01b0 => 0x75, // Latin Small Letter U With Horn | |
| 223 | 0x01b6 => 0x7a, // Latin Small Letter Z With Stroke | |
| 224 | 0x01c0 => 0x7c, // Latin Letter Dental Click | |
| 225 | 0x01c3 => 0x21, // Latin Letter Retroflex Click | |
| 226 | 0x01cd => 0x41, // Latin Capital Letter A With Caron | |
| 227 | 0x01ce => 0x61, // Latin Small Letter A With Caron | |
| 228 | 0x01cf => 0x49, // Latin Capital Letter I With Caron | |
| 229 | 0x01d0 => 0x69, // Latin Small Letter I With Caron | |
| 230 | 0x01d1 => 0x4f, // Latin Capital Letter O With Caron | |
| 231 | 0x01d2 => 0x6f, // Latin Small Letter O With Caron | |
| 232 | 0x01d3 => 0x55, // Latin Capital Letter U With Caron | |
| 233 | 0x01d4 => 0x75, // Latin Small Letter U With Caron | |
| 234 | 0x01d5 => 0x55, // Latin Capital Letter U With Diaeresis And Macron | |
| 235 | 0x01d6 => 0x75, // Latin Small Letter U With Diaeresis And Macron | |
| 236 | 0x01d7 => 0x55, // Latin Capital Letter U With Diaeresis And Acute | |
| 237 | 0x01d8 => 0x75, // Latin Small Letter U With Diaeresis And Acute | |
| 238 | 0x01d9 => 0x55, // Latin Capital Letter U With Diaeresis And Caron | |
| 239 | 0x01da => 0x75, // Latin Small Letter U With Diaeresis And Caron | |
| 240 | 0x01db => 0x55, // Latin Capital Letter U With Diaeresis And Grave | |
| 241 | 0x01dc => 0x75, // Latin Small Letter U With Diaeresis And Grave | |
| 242 | 0x01de => 0x41, // Latin Capital Letter A With Diaeresis And Macron | |
| 243 | 0x01df => 0x61, // Latin Small Letter A With Diaeresis And Macron | |
| 244 | 0x01e4 => 0x47, // Latin Capital Letter G With Stroke | |
| 245 | 0x01e5 => 0x67, // Latin Small Letter G With Stroke | |
| 246 | 0x01e6 => 0x47, // Latin Capital Letter G With Caron | |
| 247 | 0x01e7 => 0x67, // Latin Small Letter G With Caron | |
| 248 | 0x01e8 => 0x4b, // Latin Capital Letter K With Caron | |
| 249 | 0x01e9 => 0x6b, // Latin Small Letter K With Caron | |
| 250 | 0x01ea => 0x4f, // Latin Capital Letter O With Ogonek | |
| 251 | 0x01eb => 0x6f, // Latin Small Letter O With Ogonek | |
| 252 | 0x01ec => 0x4f, // Latin Capital Letter O With Ogonek And Macron | |
| 253 | 0x01ed => 0x6f, // Latin Small Letter O With Ogonek And Macron | |
| 254 | 0x01f0 => 0x6a, // Latin Small Letter J With Caron | |
| 255 | 0x0261 => 0x67, // Latin Small Letter Script G | |
| 256 | 0x02b9 => 0x27, // Modifier Letter Prime | |
| 257 | 0x02ba => 0x22, // Modifier Letter Double Prime | |
| 258 | 0x02bc => 0x27, // Modifier Letter Apostrophe | |
| 259 | 0x02c4 => 0x5e, // Modifier Letter Up Arrowhead | |
| 260 | 0x02c6 => 0x88, // Modifier Letter Circumflex Accent | |
| 261 | 0x02c8 => 0x27, // Modifier Letter Vertical Line | |
| 262 | 0x02c9 => 0xaf, // Modifier Letter Macron | |
| 263 | 0x02ca => 0xb4, // Modifier Letter Acute Accent | |
| 264 | 0x02cb => 0x60, // Modifier Letter Grave Accent | |
| 265 | 0x02cd => 0x5f, // Modifier Letter Low Macron | |
| 266 | 0x02da => 0xb0, // Ring Above | |
| 267 | 0x02dc => 0x98, // Small Tilde | |
| 268 | 0x0300 => 0x60, // Combining Grave Accent | |
| 269 | 0x0301 => 0xb4, // Combining Acute Accent | |
| 270 | 0x0302 => 0x5e, // Combining Circumflex Accent | |
| 271 | 0x0303 => 0x7e, // Combining Tilde | |
| 272 | 0x0304 => 0xaf, // Combining Macron | |
| 273 | 0x0305 => 0xaf, // Combining Overline | |
| 274 | 0x0308 => 0xa8, // Combining Diaeresis | |
| 275 | 0x030a => 0xb0, // Combining Ring Above | |
| 276 | 0x030e => 0x22, // Combining Double Vertical Line Above | |
| 277 | 0x0327 => 0xb8, // Combining Cedilla | |
| 278 | 0x0331 => 0x5f, // Combining Macron Below | |
| 279 | 0x0332 => 0x5f, // Combining Low Line | |
| 280 | 0x037e => 0x3b, // Greek Question Mark | |
| 281 | 0x0393 => 0x47, // Greek Capital Letter Gamma | |
| 282 | 0x0398 => 0x54, // Greek Capital Letter Theta | |
| 283 | 0x03a3 => 0x53, // Greek Capital Letter Sigma | |
| 284 | 0x03a6 => 0x46, // Greek Capital Letter Phi | |
| 285 | 0x03a9 => 0x4f, // Greek Capital Letter Omega | |
| 286 | 0x03b1 => 0x61, // Greek Small Letter Alpha | |
| 287 | 0x03b2 => 0xdf, // Greek Small Letter Beta | |
| 288 | 0x03b4 => 0x64, // Greek Small Letter Delta | |
| 289 | 0x03b5 => 0x65, // Greek Small Letter Epsilon | |
| 290 | 0x03bc => 0xb5, // Greek Small Letter Mu | |
| 291 | 0x03c0 => 0x70, // Greek Small Letter Pi | |
| 292 | 0x03c3 => 0x73, // Greek Small Letter Sigma | |
| 293 | 0x03c4 => 0x74, // Greek Small Letter Tau | |
| 294 | 0x03c6 => 0x66, // Greek Small Letter Phi | |
| 295 | 0x04bb => 0x68, // Cyrillic Small Letter Shha | |
| 296 | 0x0589 => 0x3a, // Armenian Full Stop | |
| 297 | 0x066a => 0x25, // Arabic Percent Sign | |
| 298 | 0x2000 => 0x20, // En Quad | |
| 299 | 0x2001 => 0x20, // Em Quad | |
| 300 | 0x2002 => 0x20, // En Space | |
| 301 | 0x2003 => 0x20, // Em Space | |
| 302 | 0x2004 => 0x20, // Three-Per-Em Space | |
| 303 | 0x2005 => 0x20, // Four-Per-Em Space | |
| 304 | 0x2006 => 0x20, // Six-Per-Em Space | |
| 305 | 0x2010 => 0x2d, // Hyphen | |
| 306 | 0x2011 => 0x2d, // Non-Breaking Hyphen | |
| 307 | 0x2013 => 0x96, // En Dash | |
| 308 | 0x2014 => 0x97, // Em Dash | |
| 309 | 0x2017 => 0x3d, // Double Low Line | |
| 310 | 0x2018 => 0x91, // Left Single Quotation Mark | |
| 311 | 0x2019 => 0x92, // Right Single Quotation Mark | |
| 312 | 0x201a => 0x82, // Single Low-9 Quotation Mark | |
| 313 | 0x201c => 0x93, // Left Double Quotation Mark | |
| 314 | 0x201d => 0x94, // Right Double Quotation Mark | |
| 315 | 0x201e => 0x84, // Double Low-9 Quotation Mark | |
| 316 | 0x2020 => 0x86, // Dagger | |
| 317 | 0x2021 => 0x87, // Double Dagger | |
| 318 | 0x2022 => 0x95, // Bullet | |
| 319 | 0x2024 => 0xb7, // One Dot Leader | |
| 320 | 0x2026 => 0x85, // Horizontal Ellipsis | |
| 321 | 0x2030 => 0x89, // Per Mille Sign | |
| 322 | 0x2032 => 0x27, // Prime | |
| 323 | 0x2035 => 0x60, // Reversed Prime | |
| 324 | 0x2039 => 0x8b, // Single Left-Pointing Angle Quotation Mark | |
| 325 | 0x203a => 0x9b, // Single Right-Pointing Angle Quotation Mark | |
| 326 | 0x2044 => 0x2f, // Fraction Slash | |
| 327 | 0x2070 => 0xb0, // Superscript Zero | |
| 328 | 0x2074 => 0x34, // Superscript Four | |
| 329 | 0x2075 => 0x35, // Superscript Five | |
| 330 | 0x2076 => 0x36, // Superscript Six | |
| 331 | 0x2077 => 0x37, // Superscript Seven | |
| 332 | 0x2078 => 0x38, // Superscript Eight | |
| 333 | 0x207f => 0x6e, // Superscript Latin Small Letter N | |
| 334 | 0x2080 => 0x30, // Subscript Zero | |
| 335 | 0x2081 => 0x31, // Subscript One | |
| 336 | 0x2082 => 0x32, // Subscript Two | |
| 337 | 0x2083 => 0x33, // Subscript Three | |
| 338 | 0x2084 => 0x34, // Subscript Four | |
| 339 | 0x2085 => 0x35, // Subscript Five | |
| 340 | 0x2086 => 0x36, // Subscript Six | |
| 341 | 0x2087 => 0x37, // Subscript Seven | |
| 342 | 0x2088 => 0x38, // Subscript Eight | |
| 343 | 0x2089 => 0x39, // Subscript Nine | |
| 344 | 0x20ac => 0x80, // Euro Sign | |
| 345 | 0x20a1 => 0xa2, // Colon Sign | |
| 346 | 0x20a4 => 0xa3, // Lira Sign | |
| 347 | 0x20a7 => 0x50, // Peseta Sign | |
| 348 | 0x2102 => 0x43, // Double-Struck Capital C | |
| 349 | 0x2107 => 0x45, // Euler Constant | |
| 350 | 0x210a => 0x67, // Script Small G | |
| 351 | 0x210b => 0x48, // Script Capital H | |
| 352 | 0x210c => 0x48, // Black-Letter Capital H | |
| 353 | 0x210d => 0x48, // Double-Struck Capital H | |
| 354 | 0x210e => 0x68, // Planck Constant | |
| 355 | 0x2110 => 0x49, // Script Capital I | |
| 356 | 0x2111 => 0x49, // Black-Letter Capital I | |
| 357 | 0x2112 => 0x4c, // Script Capital L | |
| 358 | 0x2113 => 0x6c, // Script Small L | |
| 359 | 0x2115 => 0x4e, // Double-Struck Capital N | |
| 360 | 0x2118 => 0x50, // Script Capital P | |
| 361 | 0x2119 => 0x50, // Double-Struck Capital P | |
| 362 | 0x211a => 0x51, // Double-Struck Capital Q | |
| 363 | 0x211b => 0x52, // Script Capital R | |
| 364 | 0x211c => 0x52, // Black-Letter Capital R | |
| 365 | 0x211d => 0x52, // Double-Struck Capital R | |
| 366 | 0x2122 => 0x99, // Trade Mark Sign | |
| 367 | 0x2124 => 0x5a, // Double-Struck Capital Z | |
| 368 | 0x2128 => 0x5a, // Black-Letter Capital Z | |
| 369 | 0x212a => 0x4b, // Kelvin Sign | |
| 370 | 0x212b => 0xc5, // Angstrom Sign | |
| 371 | 0x212c => 0x42, // Script Capital B | |
| 372 | 0x212d => 0x43, // Black-Letter Capital C | |
| 373 | 0x212e => 0x65, // Estimated Symbol | |
| 374 | 0x212f => 0x65, // Script Small E | |
| 375 | 0x2130 => 0x45, // Script Capital E | |
| 376 | 0x2131 => 0x46, // Script Capital F | |
| 377 | 0x2133 => 0x4d, // Script Capital M | |
| 378 | 0x2134 => 0x6f, // Script Small O | |
| 379 | 0x2205 => 0xd8, // Empty Set | |
| 380 | 0x2212 => 0x2d, // Minus Sign | |
| 381 | 0x2213 => 0xb1, // Minus-Or-Plus Sign | |
| 382 | 0x2215 => 0x2f, // Division Slash | |
| 383 | 0x2216 => 0x5c, // Set Minus | |
| 384 | 0x2217 => 0x2a, // Asterisk Operator | |
| 385 | 0x2218 => 0xb0, // Ring Operator | |
| 386 | 0x2219 => 0xb7, // Bullet Operator | |
| 387 | 0x221a => 0x76, // Square Root | |
| 388 | 0x221e => 0x38, // Infinity | |
| 389 | 0x2223 => 0x7c, // Divides | |
| 390 | 0x2229 => 0x6e, // Intersection | |
| 391 | 0x2236 => 0x3a, // Ratio | |
| 392 | 0x223c => 0x7e, // Tilde Operator | |
| 393 | 0x2248 => 0x98, // Almost Equal To | |
| 394 | 0x2261 => 0x3d, // Identical To | |
| 395 | 0x2264 => 0x3d, // Less-Than Or Equal To | |
| 396 | 0x2265 => 0x3d, // Greater-Than Or Equal To | |
| 397 | 0x226a => 0xab, // Much Less-Than | |
| 398 | 0x226b => 0xbb, // Much Greater-Than | |
| 399 | 0x22c5 => 0xb7, // Dot Operator | |
| 400 | 0x2302 => 0xa6, // House | |
| 401 | 0x2303 => 0x5e, // Up Arrowhead | |
| 402 | 0x2310 => 0xac, // Reversed Not Sign | |
| 403 | 0x2320 => 0x28, // Top Half Integral | |
| 404 | 0x2321 => 0x29, // Bottom Half Integral | |
| 405 | 0x2329 => 0x3c, // Left-Pointing Angle Bracket | |
| 406 | 0x232a => 0x3e, // Right-Pointing Angle Bracket | |
| 407 | 0x2500 => 0x2d, // Box Drawings Light Horizontal | |
| 408 | 0x2502 => 0xa6, // Box Drawings Light Vertical | |
| 409 | 0x250c => 0x2b, // Box Drawings Light Down And Right | |
| 410 | 0x2510 => 0x2b, // Box Drawings Light Down And Left | |
| 411 | 0x2514 => 0x2b, // Box Drawings Light Up And Right | |
| 412 | 0x2518 => 0x2b, // Box Drawings Light Up And Left | |
| 413 | 0x251c => 0x2b, // Box Drawings Light Vertical And Right | |
| 414 | 0x2524 => 0xa6, // Box Drawings Light Vertical And Left | |
| 415 | 0x252c => 0x2d, // Box Drawings Light Down And Horizontal | |
| 416 | 0x2534 => 0x2d, // Box Drawings Light Up And Horizontal | |
| 417 | 0x253c => 0x2b, // Box Drawings Light Vertical And Horizontal | |
| 418 | 0x2550 => 0x2d, // Box Drawings Double Horizontal | |
| 419 | 0x2551 => 0xa6, // Box Drawings Double Vertical | |
| 420 | 0x2552 => 0x2b, // Box Drawings Down Single And Right Double | |
| 421 | 0x2553 => 0x2b, // Box Drawings Down Double And Right Single | |
| 422 | 0x2554 => 0x2b, // Box Drawings Double Down And Right | |
| 423 | 0x2555 => 0x2b, // Box Drawings Down Single And Left Double | |
| 424 | 0x2556 => 0x2b, // Box Drawings Down Double And Left Single | |
| 425 | 0x2557 => 0x2b, // Box Drawings Double Down And Left | |
| 426 | 0x2558 => 0x2b, // Box Drawings Up Single And Right Double | |
| 427 | 0x2559 => 0x2b, // Box Drawings Up Double And Right Single | |
| 428 | 0x255a => 0x2b, // Box Drawings Double Up And Right | |
| 429 | 0x255b => 0x2b, // Box Drawings Up Single And Left Double | |
| 430 | 0x255c => 0x2b, // Box Drawings Up Double And Left Single | |
| 431 | 0x255d => 0x2b, // Box Drawings Double Up And Left | |
| 432 | 0x255e => 0xa6, // Box Drawings Vertical Single And Right Double | |
| 433 | 0x255f => 0xa6, // Box Drawings Vertical Double And Right Single | |
| 434 | 0x2560 => 0xa6, // Box Drawings Double Vertical And Right | |
| 435 | 0x2561 => 0xa6, // Box Drawings Vertical Single And Left Double | |
| 436 | 0x2562 => 0xa6, // Box Drawings Vertical Double And Left Single | |
| 437 | 0x2563 => 0xa6, // Box Drawings Double Vertical And Left | |
| 438 | 0x2564 => 0x2d, // Box Drawings Down Single And Horizontal Double | |
| 439 | 0x2565 => 0x2d, // Box Drawings Down Double And Horizontal Single | |
| 440 | 0x2566 => 0x2d, // Box Drawings Double Down And Horizontal | |
| 441 | 0x2567 => 0x2d, // Box Drawings Up Single And Horizontal Double | |
| 442 | 0x2568 => 0x2d, // Box Drawings Up Double And Horizontal Single | |
| 443 | 0x2569 => 0x2d, // Box Drawings Double Up And Horizontal | |
| 444 | 0x256a => 0x2b, // Box Drawings Vertical Single And Horizontal Double | |
| 445 | 0x256b => 0x2b, // Box Drawings Vertical Double And Horizontal Single | |
| 446 | 0x256c => 0x2b, // Box Drawings Double Vertical And Horizontal | |
| 447 | 0x2580 => 0xaf, // Upper Half Block | |
| 448 | 0x2584 => 0x5f, // Lower Half Block | |
| 449 | 0x2588 => 0xa6, // Full Block | |
| 450 | 0x258c => 0xa6, // Left Half Block | |
| 451 | 0x2590 => 0xa6, // Right Half Block | |
| 452 | 0x2591 => 0xa6, // Light Shade | |
| 453 | 0x2592 => 0xa6, // Medium Shade | |
| 454 | 0x2593 => 0xa6, // Dark Shade | |
| 455 | 0x25a0 => 0xa6, // Black Square | |
| 456 | 0x263c => 0xa4, // White Sun With Rays | |
| 457 | 0x2758 => 0x7c, // Light Vertical Bar | |
| 458 | 0x3000 => 0x20, // Ideographic Space | |
| 459 | 0x3008 => 0x3c, // Left Angle Bracket | |
| 460 | 0x3009 => 0x3e, // Right Angle Bracket | |
| 461 | 0x300a => 0xab, // Left Double Angle Bracket | |
| 462 | 0x300b => 0xbb, // Right Double Angle Bracket | |
| 463 | 0x301a => 0x5b, // Left White Square Bracket | |
| 464 | 0x301b => 0x5d, // Right White Square Bracket | |
| 465 | 0x30fb => 0xb7, // Katakana Middle Dot | |
| 466 | 0xff01 => 0x21, // Fullwidth Exclamation Mark | |
| 467 | 0xff02 => 0x22, // Fullwidth Quotation Mark | |
| 468 | 0xff03 => 0x23, // Fullwidth Number Sign | |
| 469 | 0xff04 => 0x24, // Fullwidth Dollar Sign | |
| 470 | 0xff05 => 0x25, // Fullwidth Percent Sign | |
| 471 | 0xff06 => 0x26, // Fullwidth Ampersand | |
| 472 | 0xff07 => 0x27, // Fullwidth Apostrophe | |
| 473 | 0xff08 => 0x28, // Fullwidth Left Parenthesis | |
| 474 | 0xff09 => 0x29, // Fullwidth Right Parenthesis | |
| 475 | 0xff0a => 0x2a, // Fullwidth Asterisk | |
| 476 | 0xff0b => 0x2b, // Fullwidth Plus Sign | |
| 477 | 0xff0c => 0x2c, // Fullwidth Comma | |
| 478 | 0xff0d => 0x2d, // Fullwidth Hyphen-Minus | |
| 479 | 0xff0e => 0x2e, // Fullwidth Full Stop | |
| 480 | 0xff0f => 0x2f, // Fullwidth Solidus | |
| 481 | 0xff10 => 0x30, // Fullwidth Digit Zero | |
| 482 | 0xff11 => 0x31, // Fullwidth Digit One | |
| 483 | 0xff12 => 0x32, // Fullwidth Digit Two | |
| 484 | 0xff13 => 0x33, // Fullwidth Digit Three | |
| 485 | 0xff14 => 0x34, // Fullwidth Digit Four | |
| 486 | 0xff15 => 0x35, // Fullwidth Digit Five | |
| 487 | 0xff16 => 0x36, // Fullwidth Digit Six | |
| 488 | 0xff17 => 0x37, // Fullwidth Digit Seven | |
| 489 | 0xff18 => 0x38, // Fullwidth Digit Eight | |
| 490 | 0xff19 => 0x39, // Fullwidth Digit Nine | |
| 491 | 0xff1a => 0x3a, // Fullwidth Colon | |
| 492 | 0xff1b => 0x3b, // Fullwidth Semicolon | |
| 493 | 0xff1c => 0x3c, // Fullwidth Less-Than Sign | |
| 494 | 0xff1d => 0x3d, // Fullwidth Equals Sign | |
| 495 | 0xff1e => 0x3e, // Fullwidth Greater-Than Sign | |
| 496 | 0xff1f => 0x3f, // Fullwidth Question Mark | |
| 497 | 0xff20 => 0x40, // Fullwidth Commercial At | |
| 498 | 0xff21 => 0x41, // Fullwidth Latin Capital Letter A | |
| 499 | 0xff22 => 0x42, // Fullwidth Latin Capital Letter B | |
| 500 | 0xff23 => 0x43, // Fullwidth Latin Capital Letter C | |
| 501 | 0xff24 => 0x44, // Fullwidth Latin Capital Letter D | |
| 502 | 0xff25 => 0x45, // Fullwidth Latin Capital Letter E | |
| 503 | 0xff26 => 0x46, // Fullwidth Latin Capital Letter F | |
| 504 | 0xff27 => 0x47, // Fullwidth Latin Capital Letter G | |
| 505 | 0xff28 => 0x48, // Fullwidth Latin Capital Letter H | |
| 506 | 0xff29 => 0x49, // Fullwidth Latin Capital Letter I | |
| 507 | 0xff2a => 0x4a, // Fullwidth Latin Capital Letter J | |
| 508 | 0xff2b => 0x4b, // Fullwidth Latin Capital Letter K | |
| 509 | 0xff2c => 0x4c, // Fullwidth Latin Capital Letter L | |
| 510 | 0xff2d => 0x4d, // Fullwidth Latin Capital Letter M | |
| 511 | 0xff2e => 0x4e, // Fullwidth Latin Capital Letter N | |
| 512 | 0xff2f => 0x4f, // Fullwidth Latin Capital Letter O | |
| 513 | 0xff30 => 0x50, // Fullwidth Latin Capital Letter P | |
| 514 | 0xff31 => 0x51, // Fullwidth Latin Capital Letter Q | |
| 515 | 0xff32 => 0x52, // Fullwidth Latin Capital Letter R | |
| 516 | 0xff33 => 0x53, // Fullwidth Latin Capital Letter S | |
| 517 | 0xff34 => 0x54, // Fullwidth Latin Capital Letter T | |
| 518 | 0xff35 => 0x55, // Fullwidth Latin Capital Letter U | |
| 519 | 0xff36 => 0x56, // Fullwidth Latin Capital Letter V | |
| 520 | 0xff37 => 0x57, // Fullwidth Latin Capital Letter W | |
| 521 | 0xff38 => 0x58, // Fullwidth Latin Capital Letter X | |
| 522 | 0xff39 => 0x59, // Fullwidth Latin Capital Letter Y | |
| 523 | 0xff3a => 0x5a, // Fullwidth Latin Capital Letter Z | |
| 524 | 0xff3b => 0x5b, // Fullwidth Left Square Bracket | |
| 525 | 0xff3c => 0x5c, // Fullwidth Reverse Solidus | |
| 526 | 0xff3d => 0x5d, // Fullwidth Right Square Bracket | |
| 527 | 0xff3e => 0x5e, // Fullwidth Circumflex Accent | |
| 528 | 0xff3f => 0x5f, // Fullwidth Low Line | |
| 529 | 0xff40 => 0x60, // Fullwidth Grave Accent | |
| 530 | 0xff41 => 0x61, // Fullwidth Latin Small Letter A | |
| 531 | 0xff42 => 0x62, // Fullwidth Latin Small Letter B | |
| 532 | 0xff43 => 0x63, // Fullwidth Latin Small Letter C | |
| 533 | 0xff44 => 0x64, // Fullwidth Latin Small Letter D | |
| 534 | 0xff45 => 0x65, // Fullwidth Latin Small Letter E | |
| 535 | 0xff46 => 0x66, // Fullwidth Latin Small Letter F | |
| 536 | 0xff47 => 0x67, // Fullwidth Latin Small Letter G | |
| 537 | 0xff48 => 0x68, // Fullwidth Latin Small Letter H | |
| 538 | 0xff49 => 0x69, // Fullwidth Latin Small Letter I | |
| 539 | 0xff4a => 0x6a, // Fullwidth Latin Small Letter J | |
| 540 | 0xff4b => 0x6b, // Fullwidth Latin Small Letter K | |
| 541 | 0xff4c => 0x6c, // Fullwidth Latin Small Letter L | |
| 542 | 0xff4d => 0x6d, // Fullwidth Latin Small Letter M | |
| 543 | 0xff4e => 0x6e, // Fullwidth Latin Small Letter N | |
| 544 | 0xff4f => 0x6f, // Fullwidth Latin Small Letter O | |
| 545 | 0xff50 => 0x70, // Fullwidth Latin Small Letter P | |
| 546 | 0xff51 => 0x71, // Fullwidth Latin Small Letter Q | |
| 547 | 0xff52 => 0x72, // Fullwidth Latin Small Letter R | |
| 548 | 0xff53 => 0x73, // Fullwidth Latin Small Letter S | |
| 549 | 0xff54 => 0x74, // Fullwidth Latin Small Letter T | |
| 550 | 0xff55 => 0x75, // Fullwidth Latin Small Letter U | |
| 551 | 0xff56 => 0x76, // Fullwidth Latin Small Letter V | |
| 552 | 0xff57 => 0x77, // Fullwidth Latin Small Letter W | |
| 553 | 0xff58 => 0x78, // Fullwidth Latin Small Letter X | |
| 554 | 0xff59 => 0x79, // Fullwidth Latin Small Letter Y | |
| 555 | 0xff5a => 0x7a, // Fullwidth Latin Small Letter Z | |
| 556 | 0xff5b => 0x7b, // Fullwidth Left Curly Bracket | |
| 557 | 0xff5c => 0x7c, // Fullwidth Vertical Line | |
| 558 | 0xff5d => 0x7d, // Fullwidth Right Curly Bracket | |
| 559 | 0xff5e => 0x7e, // Fullwidth Tilde | |
| 560 | // Not in the best fit mapping, but RC uses these mappings too | |
| 561 | 0x2007 => 0xA0, // Figure Space | |
| 562 | 0x2008 => ' ', // Punctuation Space | |
| 563 | 0x2009 => ' ', // Thin Space | |
| 564 | 0x200A => ' ', // Hair Space | |
| 565 | 0x2012 => '-', // Figure Dash | |
| 566 | 0x2015 => '-', // Horizontal Bar | |
| 567 | 0x201B => '\'', // Single High-reversed-9 Quotation Mark | |
| 568 | 0x201F => '"', // Double High-reversed-9 Quotation Mark | |
| 569 | 0x202F => 0xA0, // Narrow No-Break Space | |
| 570 | 0x2033 => '"', // Double Prime | |
| 571 | 0x2036 => '"', // Reversed Double Prime | |
| 572 | else => null, | |
| 573 | }; | |
| 574 | } | |
| 575 | ||
| 576 | test "windows-1252 to utf8" { | |
| 577 | var buf = std.ArrayList(u8).init(std.testing.allocator); | |
| 578 | defer buf.deinit(); | |
| 579 | ||
| 580 | const input_windows1252 = "\x81pqrstuvwxyz{|}~\x80\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8e\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9e\x9f\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"; | |
| 581 | const expected_utf8 = "\xc2\x81pqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"; | |
| 582 | ||
| 583 | var fbs = std.io.fixedBufferStream(input_windows1252); | |
| 584 | const bytes_written = try windows1252ToUtf8Stream(buf.writer(), fbs.reader()); | |
| 585 | ||
| 586 | try std.testing.expectEqualStrings(expected_utf8, buf.items); | |
| 587 | try std.testing.expectEqual(expected_utf8.len, bytes_written); | |
| 588 | } |
lib/std/zig/ErrorBundle.zig+123-3| ... | ... | @@ -433,7 +433,9 @@ pub const Wip = struct { |
| 433 | 433 | // The ensureUnusedCapacity call above guarantees this. |
| 434 | 434 | const notes_start = wip.reserveNotes(@intCast(other_list.len)) catch unreachable; |
| 435 | 435 | for (notes_start.., other_list) |note, message| { |
| 436 | wip.extra.items[note] = @intFromEnum(wip.addOtherMessage(other, message) catch unreachable); | |
| 436 | // This line can cause `wip.extra.items` to be resized. | |
| 437 | const note_index = @intFromEnum(wip.addOtherMessage(other, message) catch unreachable); | |
| 438 | wip.extra.items[note] = note_index; | |
| 437 | 439 | } |
| 438 | 440 | } |
| 439 | 441 | |
| ... | ... | @@ -522,7 +524,8 @@ pub const Wip = struct { |
| 522 | 524 | }; |
| 523 | 525 | const loc = std.zig.findLineColumn(source, span.main); |
| 524 | 526 | |
| 525 | eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{ | |
| 527 | // This line can cause `wip.extra.items` to be resized. | |
| 528 | const note_index = @intFromEnum(try eb.addErrorMessage(.{ | |
| 526 | 529 | .msg = try eb.addString(msg), |
| 527 | 530 | .src_loc = try eb.addSourceLocation(.{ |
| 528 | 531 | .src_path = try eb.addString(src_path), |
| ... | ... | @@ -538,6 +541,7 @@ pub const Wip = struct { |
| 538 | 541 | }), |
| 539 | 542 | .notes_len = 0, // TODO rework this function to be recursive |
| 540 | 543 | })); |
| 544 | eb.extra.items[note_i] = note_index; | |
| 541 | 545 | } |
| 542 | 546 | } |
| 543 | 547 | } |
| ... | ... | @@ -567,6 +571,28 @@ pub const Wip = struct { |
| 567 | 571 | if (index == .none) return .none; |
| 568 | 572 | const other_sl = other.getSourceLocation(index); |
| 569 | 573 | |
| 574 | var ref_traces: std.ArrayListUnmanaged(ReferenceTrace) = .{}; | |
| 575 | defer ref_traces.deinit(wip.gpa); | |
| 576 | ||
| 577 | if (other_sl.reference_trace_len > 0) { | |
| 578 | var ref_index = other.extraData(SourceLocation, @intFromEnum(index)).end; | |
| 579 | for (0..other_sl.reference_trace_len) |_| { | |
| 580 | const other_ref_trace_ed = other.extraData(ReferenceTrace, ref_index); | |
| 581 | const other_ref_trace = other_ref_trace_ed.data; | |
| 582 | ref_index = other_ref_trace_ed.end; | |
| 583 | ||
| 584 | const ref_trace: ReferenceTrace = if (other_ref_trace.src_loc == .none) .{ | |
| 585 | // sentinel ReferenceTrace does not store a string index in decl_name | |
| 586 | .decl_name = other_ref_trace.decl_name, | |
| 587 | .src_loc = .none, | |
| 588 | } else .{ | |
| 589 | .decl_name = try wip.addString(other.nullTerminatedString(other_ref_trace.decl_name)), | |
| 590 | .src_loc = try wip.addOtherSourceLocation(other, other_ref_trace.src_loc), | |
| 591 | }; | |
| 592 | try ref_traces.append(wip.gpa, ref_trace); | |
| 593 | } | |
| 594 | } | |
| 595 | ||
| 570 | 596 | const src_loc = try wip.addSourceLocation(.{ |
| 571 | 597 | .src_path = try wip.addString(other.nullTerminatedString(other_sl.src_path)), |
| 572 | 598 | .line = other_sl.line, |
| ... | ... | @@ -581,7 +607,9 @@ pub const Wip = struct { |
| 581 | 607 | .reference_trace_len = other_sl.reference_trace_len, |
| 582 | 608 | }); |
| 583 | 609 | |
| 584 | // TODO: also add the reference trace | |
| 610 | for (ref_traces.items) |ref_trace| { | |
| 611 | try wip.addReferenceTrace(ref_trace); | |
| 612 | } | |
| 585 | 613 | |
| 586 | 614 | return src_loc; |
| 587 | 615 | } |
| ... | ... | @@ -615,3 +643,95 @@ pub const Wip = struct { |
| 615 | 643 | } |
| 616 | 644 | } |
| 617 | 645 | }; |
| 646 | ||
| 647 | test "addBundleAsRoots" { | |
| 648 | var bundle = bundle: { | |
| 649 | var wip: ErrorBundle.Wip = undefined; | |
| 650 | try wip.init(std.testing.allocator); | |
| 651 | errdefer wip.deinit(); | |
| 652 | ||
| 653 | var ref_traces: [3]ReferenceTrace = undefined; | |
| 654 | for (&ref_traces, 0..) |*ref_trace, i| { | |
| 655 | if (i == ref_traces.len - 1) { | |
| 656 | // sentinel reference trace | |
| 657 | ref_trace.* = .{ | |
| 658 | .decl_name = 3, // signifies 3 hidden references | |
| 659 | .src_loc = .none, | |
| 660 | }; | |
| 661 | } else { | |
| 662 | ref_trace.* = .{ | |
| 663 | .decl_name = try wip.addString("foo"), | |
| 664 | .src_loc = try wip.addSourceLocation(.{ | |
| 665 | .src_path = try wip.addString("foo"), | |
| 666 | .line = 1, | |
| 667 | .column = 2, | |
| 668 | .span_start = 3, | |
| 669 | .span_main = 4, | |
| 670 | .span_end = 5, | |
| 671 | .source_line = 0, | |
| 672 | }), | |
| 673 | }; | |
| 674 | } | |
| 675 | } | |
| 676 | ||
| 677 | const src_loc = try wip.addSourceLocation(.{ | |
| 678 | .src_path = try wip.addString("foo"), | |
| 679 | .line = 1, | |
| 680 | .column = 2, | |
| 681 | .span_start = 3, | |
| 682 | .span_main = 4, | |
| 683 | .span_end = 5, | |
| 684 | .source_line = try wip.addString("some source code"), | |
| 685 | .reference_trace_len = ref_traces.len, | |
| 686 | }); | |
| 687 | for (&ref_traces) |ref_trace| { | |
| 688 | try wip.addReferenceTrace(ref_trace); | |
| 689 | } | |
| 690 | ||
| 691 | try wip.addRootErrorMessage(ErrorMessage{ | |
| 692 | .msg = try wip.addString("hello world"), | |
| 693 | .src_loc = src_loc, | |
| 694 | .notes_len = 1, | |
| 695 | }); | |
| 696 | const i = try wip.reserveNotes(1); | |
| 697 | const note_index = @intFromEnum(wip.addErrorMessageAssumeCapacity(.{ | |
| 698 | .msg = try wip.addString("this is a note"), | |
| 699 | .src_loc = try wip.addSourceLocation(.{ | |
| 700 | .src_path = try wip.addString("bar"), | |
| 701 | .line = 1, | |
| 702 | .column = 2, | |
| 703 | .span_start = 3, | |
| 704 | .span_main = 4, | |
| 705 | .span_end = 5, | |
| 706 | .source_line = try wip.addString("another line of source"), | |
| 707 | }), | |
| 708 | })); | |
| 709 | wip.extra.items[i] = note_index; | |
| 710 | ||
| 711 | break :bundle try wip.toOwnedBundle(""); | |
| 712 | }; | |
| 713 | defer bundle.deinit(std.testing.allocator); | |
| 714 | ||
| 715 | const ttyconf: std.io.tty.Config = .no_color; | |
| 716 | ||
| 717 | var bundle_buf = std.ArrayList(u8).init(std.testing.allocator); | |
| 718 | defer bundle_buf.deinit(); | |
| 719 | try bundle.renderToWriter(.{ .ttyconf = ttyconf }, bundle_buf.writer()); | |
| 720 | ||
| 721 | var copy = copy: { | |
| 722 | var wip: ErrorBundle.Wip = undefined; | |
| 723 | try wip.init(std.testing.allocator); | |
| 724 | errdefer wip.deinit(); | |
| 725 | ||
| 726 | try wip.addBundleAsRoots(bundle); | |
| 727 | ||
| 728 | break :copy try wip.toOwnedBundle(""); | |
| 729 | }; | |
| 730 | defer copy.deinit(std.testing.allocator); | |
| 731 | ||
| 732 | var copy_buf = std.ArrayList(u8).init(std.testing.allocator); | |
| 733 | defer copy_buf.deinit(); | |
| 734 | try copy.renderToWriter(.{ .ttyconf = ttyconf }, copy_buf.writer()); | |
| 735 | ||
| 736 | try std.testing.expectEqualStrings(bundle_buf.items, copy_buf.items); | |
| 737 | } |
src/Compilation.zig+182-452| ... | ... | @@ -36,7 +36,6 @@ const Cache = std.Build.Cache; |
| 36 | 36 | const c_codegen = @import("codegen/c.zig"); |
| 37 | 37 | const libtsan = @import("libtsan.zig"); |
| 38 | 38 | const Zir = std.zig.Zir; |
| 39 | const resinator = @import("resinator.zig"); | |
| 40 | 39 | const Builtin = @import("Builtin.zig"); |
| 41 | 40 | const LlvmObject = @import("codegen/llvm.zig").Object; |
| 42 | 41 | |
| ... | ... | @@ -174,7 +173,7 @@ local_cache_directory: Directory, |
| 174 | 173 | global_cache_directory: Directory, |
| 175 | 174 | libc_include_dir_list: []const []const u8, |
| 176 | 175 | libc_framework_dir_list: []const []const u8, |
| 177 | rc_include_dir_list: []const []const u8, | |
| 176 | rc_includes: RcIncludes, | |
| 178 | 177 | thread_pool: *ThreadPool, |
| 179 | 178 | |
| 180 | 179 | /// 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 | 1242 | options.libc_installation, |
| 1244 | 1243 | ); |
| 1245 | 1244 | |
| 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 | 1245 | const sysroot = options.sysroot orelse libc_dirs.sysroot; |
| 1309 | 1246 | |
| 1310 | 1247 | const include_compiler_rt = options.want_compiler_rt orelse |
| ... | ... | @@ -1492,7 +1429,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil |
| 1492 | 1429 | .self_exe_path = options.self_exe_path, |
| 1493 | 1430 | .libc_include_dir_list = libc_dirs.libc_include_dir_list, |
| 1494 | 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 | 1433 | .thread_pool = options.thread_pool, |
| 1497 | 1434 | .clang_passthrough_mode = options.clang_passthrough_mode, |
| 1498 | 1435 | .clang_preprocessor_mode = options.clang_preprocessor_mode, |
| ... | ... | @@ -2506,7 +2443,7 @@ fn addNonIncrementalStuffToCacheManifest( |
| 2506 | 2443 | man.hash.add(comp.link_eh_frame_hdr); |
| 2507 | 2444 | man.hash.add(comp.skip_linker_dependencies); |
| 2508 | 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 | 2447 | man.hash.addListOfBytes(comp.force_undefined_symbols.keys()); |
| 2511 | 2448 | man.hash.addListOfBytes(comp.framework_dirs); |
| 2512 | 2449 | try link.hashAddSystemLibs(man, comp.system_libs); |
| ... | ... | @@ -4172,7 +4109,7 @@ pub fn obtainCObjectCacheManifest( |
| 4172 | 4109 | pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest { |
| 4173 | 4110 | var man = comp.cache_parent.obtain(); |
| 4174 | 4111 | |
| 4175 | man.hash.addListOfBytes(comp.rc_include_dir_list); | |
| 4112 | man.hash.add(comp.rc_includes); | |
| 4176 | 4113 | |
| 4177 | 4114 | return man; |
| 4178 | 4115 | } |
| ... | ... | @@ -4812,11 +4749,12 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P |
| 4812 | 4749 | } |
| 4813 | 4750 | |
| 4814 | 4751 | fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void { |
| 4815 | if (!build_options.have_llvm) { | |
| 4816 | return comp.failWin32Resource(win32_resource, "clang not available: compiler built without LLVM extensions", .{}); | |
| 4752 | if (!std.process.can_spawn) { | |
| 4753 | return comp.failWin32Resource(win32_resource, "{s} does not support spawning a child process", .{@tagName(builtin.os.tag)}); | |
| 4817 | 4754 | } |
| 4755 | ||
| 4818 | 4756 | 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", .{}); | |
| 4820 | 4758 | |
| 4821 | 4759 | const tracy_trace = trace(@src()); |
| 4822 | 4760 | defer tracy_trace.end(); |
| ... | ... | @@ -4856,6 +4794,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4856 | 4794 | if (win32_resource.src == .manifest) { |
| 4857 | 4795 | _ = try man.addFile(src_path, null); |
| 4858 | 4796 | |
| 4797 | const rc_basename = try std.fmt.allocPrint(arena, "{s}.rc", .{src_basename}); | |
| 4859 | 4798 | const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{src_basename}); |
| 4860 | 4799 | |
| 4861 | 4800 | const digest = if (try man.hit()) man.final() else blk: { |
| ... | ... | @@ -4867,17 +4806,12 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4867 | 4806 | var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{}); |
| 4868 | 4807 | defer o_dir.close(); |
| 4869 | 4808 | |
| 4870 | var output_file = o_dir.createFile(res_basename, .{}) catch |err| { | |
| 4871 | const output_file_path = try comp.local_cache_directory.join(arena, &.{ o_sub_path, res_basename }); | |
| 4872 | return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ output_file_path, @errorName(err) }); | |
| 4873 | }; | |
| 4874 | var output_file_closed = false; | |
| 4875 | defer if (!output_file_closed) output_file.close(); | |
| 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()); | |
| 4809 | const in_rc_path = try comp.local_cache_directory.join(comp.gpa, &.{ | |
| 4810 | o_sub_path, rc_basename, | |
| 4811 | }); | |
| 4812 | const out_res_path = try comp.local_cache_directory.join(comp.gpa, &.{ | |
| 4813 | o_sub_path, res_basename, | |
| 4814 | }); | |
| 4881 | 4815 | |
| 4882 | 4816 | // In .rc files, a " within a quoted string is escaped as "" |
| 4883 | 4817 | const fmtRcEscape = struct { |
| ... | ... | @@ -4899,28 +4833,24 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4899 | 4833 | // 1 is CREATEPROCESS_MANIFEST_RESOURCE_ID which is the default ID used for RT_MANIFEST resources |
| 4900 | 4834 | // 24 is RT_MANIFEST |
| 4901 | 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 | "--zig-integration", | |
| 4845 | "/:no-preprocess", | |
| 4846 | "/x", // ignore INCLUDE environment variable | |
| 4847 | "/c65001", // UTF-8 codepage | |
| 4848 | "/:auto-includes", | |
| 4849 | "none", | |
| 4850 | }); | |
| 4851 | try argv.appendSlice(&.{ "--", in_rc_path, out_res_path }); | |
| 4902 | 4852 | |
| 4903 | resinator.compile.compile(arena, input, output_buffered_stream.writer(), .{ | |
| 4904 | .cwd = std.fs.cwd(), | |
| 4905 | .diagnostics = &diagnostics, | |
| 4906 | .ignore_include_env_var = true, | |
| 4907 | .default_code_page = .utf8, | |
| 4908 | }) catch |err| switch (err) { | |
| 4909 | error.ParseError, error.CompileError => { | |
| 4910 | // Delete the output file on error | |
| 4911 | output_file.close(); | |
| 4912 | output_file_closed = true; | |
| 4913 | // Failing to delete is not really a big deal, so swallow any errors | |
| 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 | }; | |
| 4922 | ||
| 4923 | try output_buffered_stream.flush(); | |
| 4853 | try spawnZigRc(comp, win32_resource, src_basename, arena, argv.items, &child_progress_node); | |
| 4924 | 4854 | |
| 4925 | 4855 | break :blk digest; |
| 4926 | 4856 | }; |
| ... | ... | @@ -4951,9 +4881,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4951 | 4881 | const rc_basename_noext = src_basename[0 .. src_basename.len - std.fs.path.extension(src_basename).len]; |
| 4952 | 4882 | |
| 4953 | 4883 | 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 | 4884 | var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{}); |
| 4958 | 4885 | defer zig_cache_tmp_dir.close(); |
| 4959 | 4886 | |
| ... | ... | @@ -4963,193 +4890,66 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4963 | 4890 | // so we need a temporary filename. |
| 4964 | 4891 | const out_res_path = try comp.tmpFilePath(arena, res_filename); |
| 4965 | 4892 | |
| 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 | 4893 | var argv = std.ArrayList([]const u8).init(comp.gpa); |
| 4999 | 4894 | defer argv.deinit(); |
| 5000 | 4895 | |
| 5001 | try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" }); | |
| 5002 | ||
| 5003 | try resinator.preprocess.appendClangArgs(arena, &argv, options, .{ | |
| 5004 | .clang_target = null, // handled by addCCArgs | |
| 5005 | .system_include_paths = &.{}, // handled by addCCArgs | |
| 5006 | .needs_gnu_workaround = comp.getTarget().isGnu(), | |
| 5007 | .nostdinc = false, // handled by addCCArgs | |
| 4896 | const depfile_filename = try std.fmt.allocPrint(arena, "{s}.d.json", .{rc_basename_noext}); | |
| 4897 | const out_dep_path = try comp.tmpFilePath(arena, depfile_filename); | |
| 4898 | try argv.appendSlice(&.{ | |
| 4899 | self_exe_path, | |
| 4900 | "rc", | |
| 4901 | "--zig-integration", | |
| 4902 | "/:depfile", | |
| 4903 | out_dep_path, | |
| 4904 | "/:depfile-fmt", | |
| 4905 | "json", | |
| 4906 | "/x", // ignore INCLUDE environment variable | |
| 4907 | "/:auto-includes", | |
| 4908 | @tagName(comp.rc_includes), | |
| 5008 | 4909 | }); |
| 5009 | ||
| 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, | |
| 4910 | // While these defines are not normally present when calling rc.exe directly, | |
| 5019 | 4911 | // them being defined matches the behavior of how MSVC calls rc.exe which is the more |
| 5020 | 4912 | // relevant behavior in this case. |
| 5021 | try comp.addCCArgs(arena, &argv, .rc, out_dep_path, rc_src.owner); | |
| 5022 | ||
| 5023 | if (comp.verbose_cc) { | |
| 5024 | dump_argv(argv.items); | |
| 4913 | switch (rc_src.owner.optimize_mode) { | |
| 4914 | .Debug => try argv.append("-D_DEBUG"), | |
| 4915 | .ReleaseSafe => {}, | |
| 4916 | .ReleaseFast, .ReleaseSmall => try argv.append("-DNDEBUG"), | |
| 5025 | 4917 | } |
| 4918 | try argv.appendSlice(rc_src.extra_flags); | |
| 4919 | try argv.appendSlice(&.{ "--", rc_src.src_path, out_res_path }); | |
| 5026 | 4920 | |
| 5027 | if (std.process.can_spawn) { | |
| 5028 | var child = std.ChildProcess.init(argv.items, arena); | |
| 5029 | child.stdin_behavior = .Ignore; | |
| 5030 | child.stdout_behavior = .Ignore; | |
| 5031 | child.stderr_behavior = .Pipe; | |
| 5032 | ||
| 5033 | try child.spawn(); | |
| 5034 | ||
| 5035 | const stderr_reader = child.stderr.?.reader(); | |
| 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 | }; | |
| 4921 | try spawnZigRc(comp, win32_resource, src_basename, arena, argv.items, &child_progress_node); | |
| 5042 | 4922 | |
| 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 | } | |
| 5063 | ||
| 5064 | const dep_basename = std.fs.path.basename(out_dep_path); | |
| 5065 | // Add the files depended on to the cache system. | |
| 5066 | try man.addDepFilePost(zig_cache_tmp_dir, dep_basename); | |
| 5067 | switch (comp.cache_use) { | |
| 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 | }; | |
| 5079 | ||
| 5080 | const full_input = std.fs.cwd().readFileAlloc(arena, out_rcpp_path, std.math.maxInt(usize)) catch |err| switch (err) { | |
| 5081 | error.OutOfMemory => return error.OutOfMemory, | |
| 5082 | else => |e| { | |
| 5083 | return comp.failWin32Resource(win32_resource, "failed to read preprocessed file '{s}': {s}", .{ out_rcpp_path, @errorName(e) }); | |
| 5084 | }, | |
| 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(); | |
| 4923 | // Read depfile and update cache manifest | |
| 4924 | { | |
| 4925 | const dep_basename = std.fs.path.basename(out_dep_path); | |
| 4926 | const dep_file_contents = try zig_cache_tmp_dir.readFileAlloc(arena, dep_basename, 50 * 1024 * 1024); | |
| 4927 | defer arena.free(dep_file_contents); | |
| 5100 | 4928 | |
| 5101 | var dependencies_list = std.ArrayList([]const u8).init(comp.gpa); | |
| 5102 | defer { | |
| 5103 | for (dependencies_list.items) |item| { | |
| 5104 | comp.gpa.free(item); | |
| 4929 | const value = try std.json.parseFromSliceLeaky(std.json.Value, arena, dep_file_contents, .{}); | |
| 4930 | if (value != .array) { | |
| 4931 | return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{}); | |
| 5105 | 4932 | } |
| 5106 | dependencies_list.deinit(); | |
| 5107 | } | |
| 5108 | 4933 | |
| 5109 | var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); | |
| 5110 | ||
| 5111 | resinator.compile.compile(arena, final_input, output_buffered_stream.writer(), .{ | |
| 5112 | .cwd = std.fs.cwd(), | |
| 5113 | .diagnostics = &diagnostics, | |
| 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 | }; | |
| 5140 | ||
| 5141 | try output_buffered_stream.flush(); | |
| 5142 | ||
| 5143 | for (dependencies_list.items) |dep_file_path| { | |
| 5144 | try man.addFilePost(dep_file_path); | |
| 5145 | switch (comp.cache_use) { | |
| 5146 | .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| { | |
| 5147 | whole.cache_manifest_mutex.lock(); | |
| 5148 | defer whole.cache_manifest_mutex.unlock(); | |
| 5149 | try whole_cache_manifest.addFilePost(dep_file_path); | |
| 5150 | }, | |
| 5151 | .incremental => {}, | |
| 4934 | for (value.array.items) |element| { | |
| 4935 | if (element != .string) { | |
| 4936 | return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{}); | |
| 4937 | } | |
| 4938 | const dep_file_path = element.string; | |
| 4939 | try man.addFilePost(dep_file_path); | |
| 4940 | switch (comp.cache_use) { | |
| 4941 | .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| { | |
| 4942 | whole.cache_manifest_mutex.lock(); | |
| 4943 | defer whole.cache_manifest_mutex.unlock(); | |
| 4944 | try whole_cache_manifest.addFilePost(dep_file_path); | |
| 4945 | }, | |
| 4946 | .incremental => {}, | |
| 4947 | } | |
| 5152 | 4948 | } |
| 4949 | // Just to save disk space, we delete the file because it is never needed again. | |
| 4950 | zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| { | |
| 4951 | log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) }); | |
| 4952 | }; | |
| 5153 | 4953 | } |
| 5154 | 4954 | |
| 5155 | 4955 | // Rename into place. |
| ... | ... | @@ -5159,8 +4959,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 5159 | 4959 | defer o_dir.close(); |
| 5160 | 4960 | const tmp_basename = std.fs.path.basename(out_res_path); |
| 5161 | 4961 | 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 | 4962 | break :blk digest; |
| 5165 | 4963 | }; |
| 5166 | 4964 | |
| ... | ... | @@ -5186,6 +4984,106 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 5186 | 4984 | }; |
| 5187 | 4985 | } |
| 5188 | 4986 | |
| 4987 | fn spawnZigRc( | |
| 4988 | comp: *Compilation, | |
| 4989 | win32_resource: *Win32Resource, | |
| 4990 | src_basename: []const u8, | |
| 4991 | arena: Allocator, | |
| 4992 | argv: []const []const u8, | |
| 4993 | child_progress_node: *std.Progress.Node, | |
| 4994 | ) !void { | |
| 4995 | var node_name: std.ArrayListUnmanaged(u8) = .{}; | |
| 4996 | defer node_name.deinit(arena); | |
| 4997 | ||
| 4998 | var child = std.ChildProcess.init(argv, arena); | |
| 4999 | child.stdin_behavior = .Ignore; | |
| 5000 | child.stdout_behavior = .Pipe; | |
| 5001 | child.stderr_behavior = .Pipe; | |
| 5002 | ||
| 5003 | child.spawn() catch |err| { | |
| 5004 | return comp.failWin32Resource(win32_resource, "unable to spawn {s} rc: {s}", .{ argv[0], @errorName(err) }); | |
| 5005 | }; | |
| 5006 | ||
| 5007 | var poller = std.io.poll(comp.gpa, enum { stdout }, .{ | |
| 5008 | .stdout = child.stdout.?, | |
| 5009 | }); | |
| 5010 | defer poller.deinit(); | |
| 5011 | ||
| 5012 | const stdout = poller.fifo(.stdout); | |
| 5013 | ||
| 5014 | poll: while (true) { | |
| 5015 | while (stdout.readableLength() < @sizeOf(std.zig.Server.Message.Header)) { | |
| 5016 | if (!(try poller.poll())) break :poll; | |
| 5017 | } | |
| 5018 | const header = stdout.reader().readStruct(std.zig.Server.Message.Header) catch unreachable; | |
| 5019 | while (stdout.readableLength() < header.bytes_len) { | |
| 5020 | if (!(try poller.poll())) break :poll; | |
| 5021 | } | |
| 5022 | const body = stdout.readableSliceOfLen(header.bytes_len); | |
| 5023 | ||
| 5024 | switch (header.tag) { | |
| 5025 | // We expect exactly one ErrorBundle, and if any error_bundle header is | |
| 5026 | // sent then it's a fatal error. | |
| 5027 | .error_bundle => { | |
| 5028 | const EbHdr = std.zig.Server.Message.ErrorBundle; | |
| 5029 | const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body)); | |
| 5030 | const extra_bytes = | |
| 5031 | body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len]; | |
| 5032 | const string_bytes = | |
| 5033 | body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len]; | |
| 5034 | const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes); | |
| 5035 | const extra_array = try comp.gpa.alloc(u32, unaligned_extra.len); | |
| 5036 | @memcpy(extra_array, unaligned_extra); | |
| 5037 | const error_bundle = std.zig.ErrorBundle{ | |
| 5038 | .string_bytes = try comp.gpa.dupe(u8, string_bytes), | |
| 5039 | .extra = extra_array, | |
| 5040 | }; | |
| 5041 | return comp.failWin32ResourceWithOwnedBundle(win32_resource, error_bundle); | |
| 5042 | }, | |
| 5043 | .progress => { | |
| 5044 | node_name.clearRetainingCapacity(); | |
| 5045 | // <resinator> is a special string that indicates that the child | |
| 5046 | // process has reached resinator's main function | |
| 5047 | if (std.mem.eql(u8, body, "<resinator>")) { | |
| 5048 | child_progress_node.setName(src_basename); | |
| 5049 | } | |
| 5050 | // Ignore 0-length strings since if multiple zig rc commands | |
| 5051 | // are executed at the same time, only one will send progress strings | |
| 5052 | // while the other(s) will send empty strings. | |
| 5053 | else if (body.len > 0) { | |
| 5054 | try node_name.appendSlice(arena, "build 'zig rc'... "); | |
| 5055 | try node_name.appendSlice(arena, body); | |
| 5056 | child_progress_node.setName(node_name.items); | |
| 5057 | } | |
| 5058 | }, | |
| 5059 | else => {}, // ignore other messages | |
| 5060 | } | |
| 5061 | ||
| 5062 | stdout.discard(body.len); | |
| 5063 | } | |
| 5064 | ||
| 5065 | // Just in case there's a failure that didn't send an ErrorBundle (e.g. an error return trace) | |
| 5066 | const stderr_reader = child.stderr.?.reader(); | |
| 5067 | const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024); | |
| 5068 | ||
| 5069 | const term = child.wait() catch |err| { | |
| 5070 | return comp.failWin32Resource(win32_resource, "unable to wait for {s} rc: {s}", .{ argv[0], @errorName(err) }); | |
| 5071 | }; | |
| 5072 | ||
| 5073 | switch (term) { | |
| 5074 | .Exited => |code| { | |
| 5075 | if (code != 0) { | |
| 5076 | log.err("zig rc failed with stderr:\n{s}", .{stderr}); | |
| 5077 | return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code}); | |
| 5078 | } | |
| 5079 | }, | |
| 5080 | else => { | |
| 5081 | log.err("zig rc terminated with stderr:\n{s}", .{stderr}); | |
| 5082 | return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); | |
| 5083 | }, | |
| 5084 | } | |
| 5085 | } | |
| 5086 | ||
| 5189 | 5087 | pub fn tmpFilePath(comp: *Compilation, ally: Allocator, suffix: []const u8) error{OutOfMemory}![]const u8 { |
| 5190 | 5088 | const s = std.fs.path.sep_str; |
| 5191 | 5089 | const rand_int = std.crypto.random.int(u64); |
| ... | ... | @@ -5352,16 +5250,9 @@ pub fn addCCArgs( |
| 5352 | 5250 | try argv.append("-isystem"); |
| 5353 | 5251 | try argv.append(c_headers_dir); |
| 5354 | 5252 | |
| 5355 | if (ext == .rc) { | |
| 5356 | for (comp.rc_include_dir_list) |include_dir| { | |
| 5357 | try argv.append("-isystem"); | |
| 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 | } | |
| 5253 | for (comp.libc_include_dir_list) |include_dir| { | |
| 5254 | try argv.append("-isystem"); | |
| 5255 | try argv.append(include_dir); | |
| 5365 | 5256 | } |
| 5366 | 5257 | |
| 5367 | 5258 | if (target.cpu.model.llvm_name) |llvm_name| { |
| ... | ... | @@ -5726,167 +5617,6 @@ fn failWin32ResourceWithOwnedBundle( |
| 5726 | 5617 | return error.AnalysisFail; |
| 5727 | 5618 | } |
| 5728 | 5619 | |
| 5729 | fn 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 | ||
| 5787 | fn 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 | ||
| 5882 | fn 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 | ||
| 5890 | 5620 | pub const FileExt = enum { |
| 5891 | 5621 | c, |
| 5892 | 5622 | cpp, |
src/main.zig+56-275| ... | ... | @@ -291,7 +291,14 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { |
| 291 | 291 | } else if (mem.eql(u8, cmd, "translate-c")) { |
| 292 | 292 | return buildOutputType(gpa, arena, args, .translate_c); |
| 293 | 293 | } else if (mem.eql(u8, cmd, "rc")) { |
| 294 | return cmdRc(gpa, arena, args[1..]); | |
| 294 | const use_server = cmd_args.len > 0 and std.mem.eql(u8, cmd_args[0], "--zig-integration"); | |
| 295 | return jitCmd(gpa, arena, cmd_args, .{ | |
| 296 | .cmd_name = "resinator", | |
| 297 | .root_src_path = "resinator/main.zig", | |
| 298 | .depend_on_aro = true, | |
| 299 | .prepend_zig_lib_dir_path = true, | |
| 300 | .server = use_server, | |
| 301 | }); | |
| 295 | 302 | } else if (mem.eql(u8, cmd, "fmt")) { |
| 296 | 303 | return jitCmd(gpa, arena, cmd_args, .{ |
| 297 | 304 | .cmd_name = "fmt", |
| ... | ... | @@ -4625,276 +4632,6 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati |
| 4625 | 4632 | } |
| 4626 | 4633 | } |
| 4627 | 4634 | |
| 4628 | fn 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 | ||
| 4832 | const RcIncludeArgs = struct { | |
| 4833 | include_paths: []const []const u8 = &.{}, | |
| 4834 | target_abi: ?[]const u8 = null, | |
| 4835 | }; | |
| 4836 | ||
| 4837 | fn 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 | ||
| 4898 | 4635 | const usage_init = |
| 4899 | 4636 | \\Usage: zig init |
| 4900 | 4637 | \\ |
| ... | ... | @@ -5569,6 +5306,8 @@ const JitCmdOptions = struct { |
| 5569 | 5306 | prepend_zig_exe_path: bool = false, |
| 5570 | 5307 | depend_on_aro: bool = false, |
| 5571 | 5308 | capture: ?*[]u8 = null, |
| 5309 | /// Send progress and error bundles via std.zig.Server over stdout | |
| 5310 | server: bool = false, | |
| 5572 | 5311 | }; |
| 5573 | 5312 | |
| 5574 | 5313 | fn jitCmd( |
| ... | ... | @@ -5714,10 +5453,52 @@ fn jitCmd( |
| 5714 | 5453 | }; |
| 5715 | 5454 | defer comp.destroy(); |
| 5716 | 5455 | |
| 5717 | updateModule(comp, color) catch |err| switch (err) { | |
| 5718 | error.SemanticAnalyzeFail => process.exit(2), | |
| 5719 | else => |e| return e, | |
| 5720 | }; | |
| 5456 | if (options.server and !builtin.single_threaded) { | |
| 5457 | var reset: std.Thread.ResetEvent = .{}; | |
| 5458 | var progress: std.Progress = .{ | |
| 5459 | .terminal = null, | |
| 5460 | .root = .{ | |
| 5461 | .context = undefined, | |
| 5462 | .parent = null, | |
| 5463 | .name = "", | |
| 5464 | .unprotected_estimated_total_items = 0, | |
| 5465 | .unprotected_completed_items = 0, | |
| 5466 | }, | |
| 5467 | .columns_written = 0, | |
| 5468 | .prev_refresh_timestamp = 0, | |
| 5469 | .timer = null, | |
| 5470 | .done = false, | |
| 5471 | }; | |
| 5472 | const main_progress_node = &progress.root; | |
| 5473 | main_progress_node.context = &progress; | |
| 5474 | var server = std.zig.Server{ | |
| 5475 | .out = std.io.getStdOut(), | |
| 5476 | .in = undefined, // won't be receiving messages | |
| 5477 | .receive_fifo = undefined, // won't be receiving messages | |
| 5478 | }; | |
| 5479 | ||
| 5480 | var progress_thread = try std.Thread.spawn(.{}, progressThread, .{ | |
| 5481 | &progress, &server, &reset, | |
| 5482 | }); | |
| 5483 | defer { | |
| 5484 | reset.set(); | |
| 5485 | progress_thread.join(); | |
| 5486 | } | |
| 5487 | ||
| 5488 | try comp.update(main_progress_node); | |
| 5489 | ||
| 5490 | var error_bundle = try comp.getAllErrorsAlloc(); | |
| 5491 | defer error_bundle.deinit(comp.gpa); | |
| 5492 | if (error_bundle.errorMessageCount() > 0) { | |
| 5493 | try server.serveErrorBundle(error_bundle); | |
| 5494 | process.exit(2); | |
| 5495 | } | |
| 5496 | } else { | |
| 5497 | updateModule(comp, color) catch |err| switch (err) { | |
| 5498 | error.SemanticAnalyzeFail => process.exit(2), | |
| 5499 | else => |e| return e, | |
| 5500 | }; | |
| 5501 | } | |
| 5721 | 5502 | |
| 5722 | 5503 | const exe_path = try global_cache_directory.join(arena, &.{comp.cache_use.whole.bin_sub_path.?}); |
| 5723 | 5504 | child_argv.appendAssumeCapacity(exe_path); |
src/resinator.zig deleted-25| ... | ... | @@ -1,25 +0,0 @@ |
| 1 | comptime { | |
| 2 | if (@import("build_options").only_core_functionality) { | |
| 3 | @compileError("resinator included in only_core_functionality build"); | |
| 4 | } | |
| 5 | } | |
| 6 | ||
| 7 | pub const ani = @import("resinator/ani.zig"); | |
| 8 | pub const ast = @import("resinator/ast.zig"); | |
| 9 | pub const bmp = @import("resinator/bmp.zig"); | |
| 10 | pub const cli = @import("resinator/cli.zig"); | |
| 11 | pub const code_pages = @import("resinator/code_pages.zig"); | |
| 12 | pub const comments = @import("resinator/comments.zig"); | |
| 13 | pub const compile = @import("resinator/compile.zig"); | |
| 14 | pub const errors = @import("resinator/errors.zig"); | |
| 15 | pub const ico = @import("resinator/ico.zig"); | |
| 16 | pub const lang = @import("resinator/lang.zig"); | |
| 17 | pub const lex = @import("resinator/lex.zig"); | |
| 18 | pub const literals = @import("resinator/literals.zig"); | |
| 19 | pub const parse = @import("resinator/parse.zig"); | |
| 20 | pub const preprocess = @import("resinator/preprocess.zig"); | |
| 21 | pub const rc = @import("resinator/rc.zig"); | |
| 22 | pub const res = @import("resinator/res.zig"); | |
| 23 | pub const source_mapping = @import("resinator/source_mapping.zig"); | |
| 24 | pub const utils = @import("resinator/utils.zig"); | |
| 25 | pub 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 | ||
| 15 | const std = @import("std"); | |
| 16 | ||
| 17 | const AF_ICON: u32 = 1; | |
| 18 | ||
| 19 | pub fn isAnimatedIcon(reader: anytype) bool { | |
| 20 | const flags = getAniheaderFlags(reader) catch return false; | |
| 21 | return flags & AF_ICON == AF_ICON; | |
| 22 | } | |
| 23 | ||
| 24 | fn getAniheaderFlags(reader: anytype) !u32 { | |
| 25 | const riff_header = try reader.readBytesNoEof(4); | |
| 26 | if (!std.mem.eql(u8, &riff_header, "RIFF")) return error.InvalidFormat; | |
| 27 | ||
| 28 | _ = try reader.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 | |
| 48 | const ANIHEADER = extern struct { | |
| 49 | cbSizeof: u32, | |
| 50 | cFrames: u32, | |
| 51 | cSteps: u32, | |
| 52 | cx: u32, | |
| 53 | cy: u32, | |
| 54 | cBitCount: u32, | |
| 55 | cPlanes: u32, | |
| 56 | jifRate: u32, | |
| 57 | flags: u32, | |
| 58 | }; |
src/resinator/ast.zig deleted-1084| ... | ... | @@ -1,1084 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Allocator = std.mem.Allocator; | |
| 3 | const Token = @import("lex.zig").Token; | |
| 4 | const CodePage = @import("code_pages.zig").CodePage; | |
| 5 | ||
| 6 | pub const Tree = struct { | |
| 7 | node: *Node, | |
| 8 | input_code_pages: CodePageLookup, | |
| 9 | output_code_pages: CodePageLookup, | |
| 10 | ||
| 11 | /// not owned by the tree | |
| 12 | source: []const u8, | |
| 13 | ||
| 14 | arena: std.heap.ArenaAllocator.State, | |
| 15 | allocator: Allocator, | |
| 16 | ||
| 17 | pub fn deinit(self: *Tree) void { | |
| 18 | self.arena.promote(self.allocator).deinit(); | |
| 19 | } | |
| 20 | ||
| 21 | pub fn root(self: *Tree) *Node.Root { | |
| 22 | return @fieldParentPtr(Node.Root, "base", self.node); | |
| 23 | } | |
| 24 | ||
| 25 | pub fn dump(self: *Tree, writer: anytype) @TypeOf(writer).Error!void { | |
| 26 | try self.node.dump(self, writer, 0); | |
| 27 | } | |
| 28 | }; | |
| 29 | ||
| 30 | pub const CodePageLookup = struct { | |
| 31 | lookup: std.ArrayListUnmanaged(CodePage) = .{}, | |
| 32 | allocator: Allocator, | |
| 33 | default_code_page: CodePage, | |
| 34 | ||
| 35 | pub fn init(allocator: Allocator, default_code_page: CodePage) CodePageLookup { | |
| 36 | return .{ | |
| 37 | .allocator = allocator, | |
| 38 | .default_code_page = default_code_page, | |
| 39 | }; | |
| 40 | } | |
| 41 | ||
| 42 | pub fn deinit(self: *CodePageLookup) void { | |
| 43 | self.lookup.deinit(self.allocator); | |
| 44 | } | |
| 45 | ||
| 46 | /// line_num is 1-indexed | |
| 47 | pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: CodePage) !void { | |
| 48 | const index = line_num - 1; | |
| 49 | if (index >= self.lookup.items.len) { | |
| 50 | const new_size = line_num; | |
| 51 | const missing_lines_start_index = self.lookup.items.len; | |
| 52 | try self.lookup.resize(self.allocator, new_size); | |
| 53 | ||
| 54 | // If there are any gaps created, we need to fill them in with the value of the | |
| 55 | // last line before the gap. This can happen for e.g. string literals that | |
| 56 | // span multiple lines, or if the start of a file has multiple empty lines. | |
| 57 | const fill_value = if (missing_lines_start_index > 0) | |
| 58 | self.lookup.items[missing_lines_start_index - 1] | |
| 59 | else | |
| 60 | self.default_code_page; | |
| 61 | var i: usize = missing_lines_start_index; | |
| 62 | while (i < new_size - 1) : (i += 1) { | |
| 63 | self.lookup.items[i] = fill_value; | |
| 64 | } | |
| 65 | } | |
| 66 | self.lookup.items[index] = code_page; | |
| 67 | } | |
| 68 | ||
| 69 | pub fn setForToken(self: *CodePageLookup, token: Token, code_page: CodePage) !void { | |
| 70 | return self.setForLineNum(token.line_number, code_page); | |
| 71 | } | |
| 72 | ||
| 73 | /// line_num is 1-indexed | |
| 74 | pub fn getForLineNum(self: CodePageLookup, line_num: usize) CodePage { | |
| 75 | return self.lookup.items[line_num - 1]; | |
| 76 | } | |
| 77 | ||
| 78 | pub fn getForToken(self: CodePageLookup, token: Token) CodePage { | |
| 79 | return self.getForLineNum(token.line_number); | |
| 80 | } | |
| 81 | }; | |
| 82 | ||
| 83 | test "CodePageLookup" { | |
| 84 | var lookup = CodePageLookup.init(std.testing.allocator, .windows1252); | |
| 85 | defer lookup.deinit(); | |
| 86 | ||
| 87 | try lookup.setForLineNum(5, .utf8); | |
| 88 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1)); | |
| 89 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2)); | |
| 90 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3)); | |
| 91 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4)); | |
| 92 | try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5)); | |
| 93 | try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len); | |
| 94 | ||
| 95 | try lookup.setForLineNum(7, .windows1252); | |
| 96 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1)); | |
| 97 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2)); | |
| 98 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3)); | |
| 99 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4)); | |
| 100 | try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5)); | |
| 101 | try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(6)); | |
| 102 | try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(7)); | |
| 103 | try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len); | |
| 104 | } | |
| 105 | ||
| 106 | pub const Node = struct { | |
| 107 | id: Id, | |
| 108 | ||
| 109 | pub const Id = enum { | |
| 110 | root, | |
| 111 | resource_external, | |
| 112 | resource_raw_data, | |
| 113 | literal, | |
| 114 | binary_expression, | |
| 115 | grouped_expression, | |
| 116 | not_expression, | |
| 117 | accelerators, | |
| 118 | accelerator, | |
| 119 | dialog, | |
| 120 | control_statement, | |
| 121 | toolbar, | |
| 122 | menu, | |
| 123 | menu_item, | |
| 124 | menu_item_separator, | |
| 125 | menu_item_ex, | |
| 126 | popup, | |
| 127 | popup_ex, | |
| 128 | version_info, | |
| 129 | version_statement, | |
| 130 | block, | |
| 131 | block_value, | |
| 132 | block_value_value, | |
| 133 | string_table, | |
| 134 | string_table_string, | |
| 135 | language_statement, | |
| 136 | font_statement, | |
| 137 | simple_statement, | |
| 138 | invalid, | |
| 139 | ||
| 140 | pub fn Type(comptime id: Id) type { | |
| 141 | return switch (id) { | |
| 142 | .root => Root, | |
| 143 | .resource_external => ResourceExternal, | |
| 144 | .resource_raw_data => ResourceRawData, | |
| 145 | .literal => Literal, | |
| 146 | .binary_expression => BinaryExpression, | |
| 147 | .grouped_expression => GroupedExpression, | |
| 148 | .not_expression => NotExpression, | |
| 149 | .accelerators => Accelerators, | |
| 150 | .accelerator => Accelerator, | |
| 151 | .dialog => Dialog, | |
| 152 | .control_statement => ControlStatement, | |
| 153 | .toolbar => Toolbar, | |
| 154 | .menu => Menu, | |
| 155 | .menu_item => MenuItem, | |
| 156 | .menu_item_separator => MenuItemSeparator, | |
| 157 | .menu_item_ex => MenuItemEx, | |
| 158 | .popup => Popup, | |
| 159 | .popup_ex => PopupEx, | |
| 160 | .version_info => VersionInfo, | |
| 161 | .version_statement => VersionStatement, | |
| 162 | .block => Block, | |
| 163 | .block_value => BlockValue, | |
| 164 | .block_value_value => BlockValueValue, | |
| 165 | .string_table => StringTable, | |
| 166 | .string_table_string => StringTableString, | |
| 167 | .language_statement => LanguageStatement, | |
| 168 | .font_statement => FontStatement, | |
| 169 | .simple_statement => SimpleStatement, | |
| 170 | .invalid => Invalid, | |
| 171 | }; | |
| 172 | } | |
| 173 | }; | |
| 174 | ||
| 175 | pub fn cast(base: *Node, comptime id: Id) ?*id.Type() { | |
| 176 | if (base.id == id) { | |
| 177 | return @fieldParentPtr(id.Type(), "base", base); | |
| 178 | } | |
| 179 | return null; | |
| 180 | } | |
| 181 | ||
| 182 | pub const Root = struct { | |
| 183 | base: Node = .{ .id = .root }, | |
| 184 | body: []*Node, | |
| 185 | }; | |
| 186 | ||
| 187 | pub const ResourceExternal = struct { | |
| 188 | base: Node = .{ .id = .resource_external }, | |
| 189 | id: Token, | |
| 190 | type: Token, | |
| 191 | common_resource_attributes: []Token, | |
| 192 | filename: *Node, | |
| 193 | }; | |
| 194 | ||
| 195 | pub const ResourceRawData = struct { | |
| 196 | base: Node = .{ .id = .resource_raw_data }, | |
| 197 | id: Token, | |
| 198 | type: Token, | |
| 199 | common_resource_attributes: []Token, | |
| 200 | begin_token: Token, | |
| 201 | raw_data: []*Node, | |
| 202 | end_token: Token, | |
| 203 | }; | |
| 204 | ||
| 205 | pub const Literal = struct { | |
| 206 | base: Node = .{ .id = .literal }, | |
| 207 | token: Token, | |
| 208 | }; | |
| 209 | ||
| 210 | pub const BinaryExpression = struct { | |
| 211 | base: Node = .{ .id = .binary_expression }, | |
| 212 | operator: Token, | |
| 213 | left: *Node, | |
| 214 | right: *Node, | |
| 215 | }; | |
| 216 | ||
| 217 | pub const GroupedExpression = struct { | |
| 218 | base: Node = .{ .id = .grouped_expression }, | |
| 219 | open_token: Token, | |
| 220 | expression: *Node, | |
| 221 | close_token: Token, | |
| 222 | }; | |
| 223 | ||
| 224 | pub const NotExpression = struct { | |
| 225 | base: Node = .{ .id = .not_expression }, | |
| 226 | not_token: Token, | |
| 227 | number_token: Token, | |
| 228 | }; | |
| 229 | ||
| 230 | pub const Accelerators = struct { | |
| 231 | base: Node = .{ .id = .accelerators }, | |
| 232 | id: Token, | |
| 233 | type: Token, | |
| 234 | common_resource_attributes: []Token, | |
| 235 | optional_statements: []*Node, | |
| 236 | begin_token: Token, | |
| 237 | accelerators: []*Node, | |
| 238 | end_token: Token, | |
| 239 | }; | |
| 240 | ||
| 241 | pub const Accelerator = struct { | |
| 242 | base: Node = .{ .id = .accelerator }, | |
| 243 | event: *Node, | |
| 244 | idvalue: *Node, | |
| 245 | type_and_options: []Token, | |
| 246 | }; | |
| 247 | ||
| 248 | pub const Dialog = struct { | |
| 249 | base: Node = .{ .id = .dialog }, | |
| 250 | id: Token, | |
| 251 | type: Token, | |
| 252 | common_resource_attributes: []Token, | |
| 253 | x: *Node, | |
| 254 | y: *Node, | |
| 255 | width: *Node, | |
| 256 | height: *Node, | |
| 257 | help_id: ?*Node, | |
| 258 | optional_statements: []*Node, | |
| 259 | begin_token: Token, | |
| 260 | controls: []*Node, | |
| 261 | end_token: Token, | |
| 262 | }; | |
| 263 | ||
| 264 | pub const ControlStatement = struct { | |
| 265 | base: Node = .{ .id = .control_statement }, | |
| 266 | type: Token, | |
| 267 | text: ?Token, | |
| 268 | /// Only relevant for the user-defined CONTROL control | |
| 269 | class: ?*Node, | |
| 270 | id: *Node, | |
| 271 | x: *Node, | |
| 272 | y: *Node, | |
| 273 | width: *Node, | |
| 274 | height: *Node, | |
| 275 | style: ?*Node, | |
| 276 | exstyle: ?*Node, | |
| 277 | help_id: ?*Node, | |
| 278 | extra_data_begin: ?Token, | |
| 279 | extra_data: []*Node, | |
| 280 | extra_data_end: ?Token, | |
| 281 | ||
| 282 | /// Returns true if this node describes a user-defined CONTROL control | |
| 283 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/control-control | |
| 284 | pub fn isUserDefined(self: *const ControlStatement) bool { | |
| 285 | return self.class != null; | |
| 286 | } | |
| 287 | }; | |
| 288 | ||
| 289 | pub const Toolbar = struct { | |
| 290 | base: Node = .{ .id = .toolbar }, | |
| 291 | id: Token, | |
| 292 | type: Token, | |
| 293 | common_resource_attributes: []Token, | |
| 294 | button_width: *Node, | |
| 295 | button_height: *Node, | |
| 296 | begin_token: Token, | |
| 297 | /// Will contain Literal and SimpleStatement nodes | |
| 298 | buttons: []*Node, | |
| 299 | end_token: Token, | |
| 300 | }; | |
| 301 | ||
| 302 | pub const Menu = struct { | |
| 303 | base: Node = .{ .id = .menu }, | |
| 304 | id: Token, | |
| 305 | type: Token, | |
| 306 | common_resource_attributes: []Token, | |
| 307 | optional_statements: []*Node, | |
| 308 | /// `help_id` will never be non-null if `type` is MENU | |
| 309 | help_id: ?*Node, | |
| 310 | begin_token: Token, | |
| 311 | items: []*Node, | |
| 312 | end_token: Token, | |
| 313 | }; | |
| 314 | ||
| 315 | pub const MenuItem = struct { | |
| 316 | base: Node = .{ .id = .menu_item }, | |
| 317 | menuitem: Token, | |
| 318 | text: Token, | |
| 319 | result: *Node, | |
| 320 | option_list: []Token, | |
| 321 | }; | |
| 322 | ||
| 323 | pub const MenuItemSeparator = struct { | |
| 324 | base: Node = .{ .id = .menu_item_separator }, | |
| 325 | menuitem: Token, | |
| 326 | separator: Token, | |
| 327 | }; | |
| 328 | ||
| 329 | pub const MenuItemEx = struct { | |
| 330 | base: Node = .{ .id = .menu_item_ex }, | |
| 331 | menuitem: Token, | |
| 332 | text: Token, | |
| 333 | id: ?*Node, | |
| 334 | type: ?*Node, | |
| 335 | state: ?*Node, | |
| 336 | }; | |
| 337 | ||
| 338 | pub const Popup = struct { | |
| 339 | base: Node = .{ .id = .popup }, | |
| 340 | popup: Token, | |
| 341 | text: Token, | |
| 342 | option_list: []Token, | |
| 343 | begin_token: Token, | |
| 344 | items: []*Node, | |
| 345 | end_token: Token, | |
| 346 | }; | |
| 347 | ||
| 348 | pub const PopupEx = struct { | |
| 349 | base: Node = .{ .id = .popup_ex }, | |
| 350 | popup: Token, | |
| 351 | text: Token, | |
| 352 | id: ?*Node, | |
| 353 | type: ?*Node, | |
| 354 | state: ?*Node, | |
| 355 | help_id: ?*Node, | |
| 356 | begin_token: Token, | |
| 357 | items: []*Node, | |
| 358 | end_token: Token, | |
| 359 | }; | |
| 360 | ||
| 361 | pub const VersionInfo = struct { | |
| 362 | base: Node = .{ .id = .version_info }, | |
| 363 | id: Token, | |
| 364 | versioninfo: Token, | |
| 365 | common_resource_attributes: []Token, | |
| 366 | /// Will contain VersionStatement and/or SimpleStatement nodes | |
| 367 | fixed_info: []*Node, | |
| 368 | begin_token: Token, | |
| 369 | block_statements: []*Node, | |
| 370 | end_token: Token, | |
| 371 | }; | |
| 372 | ||
| 373 | /// Used for FILEVERSION and PRODUCTVERSION statements | |
| 374 | pub const VersionStatement = struct { | |
| 375 | base: Node = .{ .id = .version_statement }, | |
| 376 | type: Token, | |
| 377 | /// Between 1-4 parts | |
| 378 | parts: []*Node, | |
| 379 | }; | |
| 380 | ||
| 381 | pub const Block = struct { | |
| 382 | base: Node = .{ .id = .block }, | |
| 383 | /// The BLOCK token itself | |
| 384 | identifier: Token, | |
| 385 | key: Token, | |
| 386 | /// This is undocumented but BLOCK statements support values after | |
| 387 | /// the key just like VALUE statements. | |
| 388 | values: []*Node, | |
| 389 | begin_token: Token, | |
| 390 | children: []*Node, | |
| 391 | end_token: Token, | |
| 392 | }; | |
| 393 | ||
| 394 | pub const BlockValue = struct { | |
| 395 | base: Node = .{ .id = .block_value }, | |
| 396 | /// The VALUE token itself | |
| 397 | identifier: Token, | |
| 398 | key: Token, | |
| 399 | /// These will be BlockValueValue nodes | |
| 400 | values: []*Node, | |
| 401 | }; | |
| 402 | ||
| 403 | pub const BlockValueValue = struct { | |
| 404 | base: Node = .{ .id = .block_value_value }, | |
| 405 | expression: *Node, | |
| 406 | /// Whether or not the value has a trailing comma is relevant | |
| 407 | trailing_comma: bool, | |
| 408 | }; | |
| 409 | ||
| 410 | pub const StringTable = struct { | |
| 411 | base: Node = .{ .id = .string_table }, | |
| 412 | type: Token, | |
| 413 | common_resource_attributes: []Token, | |
| 414 | optional_statements: []*Node, | |
| 415 | begin_token: Token, | |
| 416 | strings: []*Node, | |
| 417 | end_token: Token, | |
| 418 | }; | |
| 419 | ||
| 420 | pub const StringTableString = struct { | |
| 421 | base: Node = .{ .id = .string_table_string }, | |
| 422 | id: *Node, | |
| 423 | maybe_comma: ?Token, | |
| 424 | string: Token, | |
| 425 | }; | |
| 426 | ||
| 427 | pub const LanguageStatement = struct { | |
| 428 | base: Node = .{ .id = .language_statement }, | |
| 429 | /// The LANGUAGE token itself | |
| 430 | language_token: Token, | |
| 431 | primary_language_id: *Node, | |
| 432 | sublanguage_id: *Node, | |
| 433 | }; | |
| 434 | ||
| 435 | pub const FontStatement = struct { | |
| 436 | base: Node = .{ .id = .font_statement }, | |
| 437 | /// The FONT token itself | |
| 438 | identifier: Token, | |
| 439 | point_size: *Node, | |
| 440 | typeface: Token, | |
| 441 | weight: ?*Node, | |
| 442 | italic: ?*Node, | |
| 443 | char_set: ?*Node, | |
| 444 | }; | |
| 445 | ||
| 446 | /// A statement with one value associated with it. | |
| 447 | /// Used for CAPTION, CHARACTERISTICS, CLASS, EXSTYLE, MENU, STYLE, VERSION, | |
| 448 | /// as well as VERSIONINFO-specific statements FILEFLAGSMASK, FILEFLAGS, FILEOS, | |
| 449 | /// FILETYPE, FILESUBTYPE | |
| 450 | pub const SimpleStatement = struct { | |
| 451 | base: Node = .{ .id = .simple_statement }, | |
| 452 | identifier: Token, | |
| 453 | value: *Node, | |
| 454 | }; | |
| 455 | ||
| 456 | pub const Invalid = struct { | |
| 457 | base: Node = .{ .id = .invalid }, | |
| 458 | context: []Token, | |
| 459 | }; | |
| 460 | ||
| 461 | pub fn isNumberExpression(node: *const Node) bool { | |
| 462 | switch (node.id) { | |
| 463 | .literal => { | |
| 464 | const literal = @fieldParentPtr(Node.Literal, "base", node); | |
| 465 | return switch (literal.token.id) { | |
| 466 | .number => true, | |
| 467 | else => false, | |
| 468 | }; | |
| 469 | }, | |
| 470 | .binary_expression, .grouped_expression, .not_expression => return true, | |
| 471 | else => return false, | |
| 472 | } | |
| 473 | } | |
| 474 | ||
| 475 | pub fn isStringLiteral(node: *const Node) bool { | |
| 476 | switch (node.id) { | |
| 477 | .literal => { | |
| 478 | const literal = @fieldParentPtr(Node.Literal, "base", node); | |
| 479 | return switch (literal.token.id) { | |
| 480 | .quoted_ascii_string, .quoted_wide_string => true, | |
| 481 | else => false, | |
| 482 | }; | |
| 483 | }, | |
| 484 | else => return false, | |
| 485 | } | |
| 486 | } | |
| 487 | ||
| 488 | pub fn getFirstToken(node: *const Node) Token { | |
| 489 | switch (node.id) { | |
| 490 | .root => unreachable, | |
| 491 | .resource_external => { | |
| 492 | const casted = @fieldParentPtr(Node.ResourceExternal, "base", node); | |
| 493 | return casted.id; | |
| 494 | }, | |
| 495 | .resource_raw_data => { | |
| 496 | const casted = @fieldParentPtr(Node.ResourceRawData, "base", node); | |
| 497 | return casted.id; | |
| 498 | }, | |
| 499 | .literal => { | |
| 500 | const casted = @fieldParentPtr(Node.Literal, "base", node); | |
| 501 | return casted.token; | |
| 502 | }, | |
| 503 | .binary_expression => { | |
| 504 | const casted = @fieldParentPtr(Node.BinaryExpression, "base", node); | |
| 505 | return casted.left.getFirstToken(); | |
| 506 | }, | |
| 507 | .grouped_expression => { | |
| 508 | const casted = @fieldParentPtr(Node.GroupedExpression, "base", node); | |
| 509 | return casted.open_token; | |
| 510 | }, | |
| 511 | .not_expression => { | |
| 512 | const casted = @fieldParentPtr(Node.NotExpression, "base", node); | |
| 513 | return casted.not_token; | |
| 514 | }, | |
| 515 | .accelerators => { | |
| 516 | const casted = @fieldParentPtr(Node.Accelerators, "base", node); | |
| 517 | return casted.id; | |
| 518 | }, | |
| 519 | .accelerator => { | |
| 520 | const casted = @fieldParentPtr(Node.Accelerator, "base", node); | |
| 521 | return casted.event.getFirstToken(); | |
| 522 | }, | |
| 523 | .dialog => { | |
| 524 | const casted = @fieldParentPtr(Node.Dialog, "base", node); | |
| 525 | return casted.id; | |
| 526 | }, | |
| 527 | .control_statement => { | |
| 528 | const casted = @fieldParentPtr(Node.ControlStatement, "base", node); | |
| 529 | return casted.type; | |
| 530 | }, | |
| 531 | .toolbar => { | |
| 532 | const casted = @fieldParentPtr(Node.Toolbar, "base", node); | |
| 533 | return casted.id; | |
| 534 | }, | |
| 535 | .menu => { | |
| 536 | const casted = @fieldParentPtr(Node.Menu, "base", node); | |
| 537 | return casted.id; | |
| 538 | }, | |
| 539 | inline .menu_item, .menu_item_separator, .menu_item_ex => |menu_item_type| { | |
| 540 | const node_type = menu_item_type.Type(); | |
| 541 | const casted = @fieldParentPtr(node_type, "base", node); | |
| 542 | return casted.menuitem; | |
| 543 | }, | |
| 544 | inline .popup, .popup_ex => |popup_type| { | |
| 545 | const node_type = popup_type.Type(); | |
| 546 | const casted = @fieldParentPtr(node_type, "base", node); | |
| 547 | return casted.popup; | |
| 548 | }, | |
| 549 | .version_info => { | |
| 550 | const casted = @fieldParentPtr(Node.VersionInfo, "base", node); | |
| 551 | return casted.id; | |
| 552 | }, | |
| 553 | .version_statement => { | |
| 554 | const casted = @fieldParentPtr(Node.VersionStatement, "base", node); | |
| 555 | return casted.type; | |
| 556 | }, | |
| 557 | .block => { | |
| 558 | const casted = @fieldParentPtr(Node.Block, "base", node); | |
| 559 | return casted.identifier; | |
| 560 | }, | |
| 561 | .block_value => { | |
| 562 | const casted = @fieldParentPtr(Node.BlockValue, "base", node); | |
| 563 | return casted.identifier; | |
| 564 | }, | |
| 565 | .block_value_value => { | |
| 566 | const casted = @fieldParentPtr(Node.BlockValueValue, "base", node); | |
| 567 | return casted.expression.getFirstToken(); | |
| 568 | }, | |
| 569 | .string_table => { | |
| 570 | const casted = @fieldParentPtr(Node.StringTable, "base", node); | |
| 571 | return casted.type; | |
| 572 | }, | |
| 573 | .string_table_string => { | |
| 574 | const casted = @fieldParentPtr(Node.StringTableString, "base", node); | |
| 575 | return casted.id.getFirstToken(); | |
| 576 | }, | |
| 577 | .language_statement => { | |
| 578 | const casted = @fieldParentPtr(Node.LanguageStatement, "base", node); | |
| 579 | return casted.language_token; | |
| 580 | }, | |
| 581 | .font_statement => { | |
| 582 | const casted = @fieldParentPtr(Node.FontStatement, "base", node); | |
| 583 | return casted.identifier; | |
| 584 | }, | |
| 585 | .simple_statement => { | |
| 586 | const casted = @fieldParentPtr(Node.SimpleStatement, "base", node); | |
| 587 | return casted.identifier; | |
| 588 | }, | |
| 589 | .invalid => { | |
| 590 | const casted = @fieldParentPtr(Node.Invalid, "base", node); | |
| 591 | return casted.context[0]; | |
| 592 | }, | |
| 593 | } | |
| 594 | } | |
| 595 | ||
| 596 | pub fn getLastToken(node: *const Node) Token { | |
| 597 | switch (node.id) { | |
| 598 | .root => unreachable, | |
| 599 | .resource_external => { | |
| 600 | const casted = @fieldParentPtr(Node.ResourceExternal, "base", node); | |
| 601 | return casted.filename.getLastToken(); | |
| 602 | }, | |
| 603 | .resource_raw_data => { | |
| 604 | const casted = @fieldParentPtr(Node.ResourceRawData, "base", node); | |
| 605 | return casted.end_token; | |
| 606 | }, | |
| 607 | .literal => { | |
| 608 | const casted = @fieldParentPtr(Node.Literal, "base", node); | |
| 609 | return casted.token; | |
| 610 | }, | |
| 611 | .binary_expression => { | |
| 612 | const casted = @fieldParentPtr(Node.BinaryExpression, "base", node); | |
| 613 | return casted.right.getLastToken(); | |
| 614 | }, | |
| 615 | .grouped_expression => { | |
| 616 | const casted = @fieldParentPtr(Node.GroupedExpression, "base", node); | |
| 617 | return casted.close_token; | |
| 618 | }, | |
| 619 | .not_expression => { | |
| 620 | const casted = @fieldParentPtr(Node.NotExpression, "base", node); | |
| 621 | return casted.number_token; | |
| 622 | }, | |
| 623 | .accelerators => { | |
| 624 | const casted = @fieldParentPtr(Node.Accelerators, "base", node); | |
| 625 | return casted.end_token; | |
| 626 | }, | |
| 627 | .accelerator => { | |
| 628 | const casted = @fieldParentPtr(Node.Accelerator, "base", node); | |
| 629 | if (casted.type_and_options.len > 0) return casted.type_and_options[casted.type_and_options.len - 1]; | |
| 630 | return casted.idvalue.getLastToken(); | |
| 631 | }, | |
| 632 | .dialog => { | |
| 633 | const casted = @fieldParentPtr(Node.Dialog, "base", node); | |
| 634 | return casted.end_token; | |
| 635 | }, | |
| 636 | .control_statement => { | |
| 637 | const casted = @fieldParentPtr(Node.ControlStatement, "base", node); | |
| 638 | if (casted.extra_data_end) |token| return token; | |
| 639 | if (casted.help_id) |help_id_node| return help_id_node.getLastToken(); | |
| 640 | if (casted.exstyle) |exstyle_node| return exstyle_node.getLastToken(); | |
| 641 | // For user-defined CONTROL controls, the style comes before 'x', but | |
| 642 | // otherwise it comes after 'height' so it could be the last token if | |
| 643 | // it's present. | |
| 644 | if (!casted.isUserDefined()) { | |
| 645 | if (casted.style) |style_node| return style_node.getLastToken(); | |
| 646 | } | |
| 647 | return casted.height.getLastToken(); | |
| 648 | }, | |
| 649 | .toolbar => { | |
| 650 | const casted = @fieldParentPtr(Node.Toolbar, "base", node); | |
| 651 | return casted.end_token; | |
| 652 | }, | |
| 653 | .menu => { | |
| 654 | const casted = @fieldParentPtr(Node.Menu, "base", node); | |
| 655 | return casted.end_token; | |
| 656 | }, | |
| 657 | .menu_item => { | |
| 658 | const casted = @fieldParentPtr(Node.MenuItem, "base", node); | |
| 659 | if (casted.option_list.len > 0) return casted.option_list[casted.option_list.len - 1]; | |
| 660 | return casted.result.getLastToken(); | |
| 661 | }, | |
| 662 | .menu_item_separator => { | |
| 663 | const casted = @fieldParentPtr(Node.MenuItemSeparator, "base", node); | |
| 664 | return casted.separator; | |
| 665 | }, | |
| 666 | .menu_item_ex => { | |
| 667 | const casted = @fieldParentPtr(Node.MenuItemEx, "base", node); | |
| 668 | if (casted.state) |state_node| return state_node.getLastToken(); | |
| 669 | if (casted.type) |type_node| return type_node.getLastToken(); | |
| 670 | if (casted.id) |id_node| return id_node.getLastToken(); | |
| 671 | return casted.text; | |
| 672 | }, | |
| 673 | inline .popup, .popup_ex => |popup_type| { | |
| 674 | const node_type = popup_type.Type(); | |
| 675 | const casted = @fieldParentPtr(node_type, "base", node); | |
| 676 | return casted.end_token; | |
| 677 | }, | |
| 678 | .version_info => { | |
| 679 | const casted = @fieldParentPtr(Node.VersionInfo, "base", node); | |
| 680 | return casted.end_token; | |
| 681 | }, | |
| 682 | .version_statement => { | |
| 683 | const casted = @fieldParentPtr(Node.VersionStatement, "base", node); | |
| 684 | return casted.parts[casted.parts.len - 1].getLastToken(); | |
| 685 | }, | |
| 686 | .block => { | |
| 687 | const casted = @fieldParentPtr(Node.Block, "base", node); | |
| 688 | return casted.end_token; | |
| 689 | }, | |
| 690 | .block_value => { | |
| 691 | const casted = @fieldParentPtr(Node.BlockValue, "base", node); | |
| 692 | if (casted.values.len > 0) return casted.values[casted.values.len - 1].getLastToken(); | |
| 693 | return casted.key; | |
| 694 | }, | |
| 695 | .block_value_value => { | |
| 696 | const casted = @fieldParentPtr(Node.BlockValueValue, "base", node); | |
| 697 | return casted.expression.getLastToken(); | |
| 698 | }, | |
| 699 | .string_table => { | |
| 700 | const casted = @fieldParentPtr(Node.StringTable, "base", node); | |
| 701 | return casted.end_token; | |
| 702 | }, | |
| 703 | .string_table_string => { | |
| 704 | const casted = @fieldParentPtr(Node.StringTableString, "base", node); | |
| 705 | return casted.string; | |
| 706 | }, | |
| 707 | .language_statement => { | |
| 708 | const casted = @fieldParentPtr(Node.LanguageStatement, "base", node); | |
| 709 | return casted.sublanguage_id.getLastToken(); | |
| 710 | }, | |
| 711 | .font_statement => { | |
| 712 | const casted = @fieldParentPtr(Node.FontStatement, "base", node); | |
| 713 | if (casted.char_set) |char_set_node| return char_set_node.getLastToken(); | |
| 714 | if (casted.italic) |italic_node| return italic_node.getLastToken(); | |
| 715 | if (casted.weight) |weight_node| return weight_node.getLastToken(); | |
| 716 | return casted.typeface; | |
| 717 | }, | |
| 718 | .simple_statement => { | |
| 719 | const casted = @fieldParentPtr(Node.SimpleStatement, "base", node); | |
| 720 | return casted.value.getLastToken(); | |
| 721 | }, | |
| 722 | .invalid => { | |
| 723 | const casted = @fieldParentPtr(Node.Invalid, "base", node); | |
| 724 | return casted.context[casted.context.len - 1]; | |
| 725 | }, | |
| 726 | } | |
| 727 | } | |
| 728 | ||
| 729 | pub fn dump( | |
| 730 | node: *const Node, | |
| 731 | tree: *const Tree, | |
| 732 | writer: anytype, | |
| 733 | indent: usize, | |
| 734 | ) @TypeOf(writer).Error!void { | |
| 735 | try writer.writeByteNTimes(' ', indent); | |
| 736 | try writer.writeAll(@tagName(node.id)); | |
| 737 | switch (node.id) { | |
| 738 | .root => { | |
| 739 | try writer.writeAll("\n"); | |
| 740 | const root = @fieldParentPtr(Node.Root, "base", node); | |
| 741 | for (root.body) |body_node| { | |
| 742 | try body_node.dump(tree, writer, indent + 1); | |
| 743 | } | |
| 744 | }, | |
| 745 | .resource_external => { | |
| 746 | const resource = @fieldParentPtr(Node.ResourceExternal, "base", node); | |
| 747 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len }); | |
| 748 | try resource.filename.dump(tree, writer, indent + 1); | |
| 749 | }, | |
| 750 | .resource_raw_data => { | |
| 751 | const resource = @fieldParentPtr(Node.ResourceRawData, "base", node); | |
| 752 | try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len }); | |
| 753 | for (resource.raw_data) |data_expression| { | |
| 754 | try data_expression.dump(tree, writer, indent + 1); | |
| 755 | } | |
| 756 | }, | |
| 757 | .literal => { | |
| 758 | const literal = @fieldParentPtr(Node.Literal, "base", node); | |
| 759 | try writer.writeAll(" "); | |
| 760 | try writer.writeAll(literal.token.slice(tree.source)); | |
| 761 | try writer.writeAll("\n"); | |
| 762 | }, | |
| 763 | .binary_expression => { | |
| 764 | const binary = @fieldParentPtr(Node.BinaryExpression, "base", node); | |
| 765 | try writer.writeAll(" "); | |
| 766 | try writer.writeAll(binary.operator.slice(tree.source)); | |
| 767 | try writer.writeAll("\n"); | |
| 768 | try binary.left.dump(tree, writer, indent + 1); | |
| 769 | try binary.right.dump(tree, writer, indent + 1); | |
| 770 | }, | |
| 771 | .grouped_expression => { | |
| 772 | const grouped = @fieldParentPtr(Node.GroupedExpression, "base", node); | |
| 773 | try writer.writeAll("\n"); | |
| 774 | try writer.writeByteNTimes(' ', indent); | |
| 775 | try writer.writeAll(grouped.open_token.slice(tree.source)); | |
| 776 | try writer.writeAll("\n"); | |
| 777 | try grouped.expression.dump(tree, writer, indent + 1); | |
| 778 | try writer.writeByteNTimes(' ', indent); | |
| 779 | try writer.writeAll(grouped.close_token.slice(tree.source)); | |
| 780 | try writer.writeAll("\n"); | |
| 781 | }, | |
| 782 | .not_expression => { | |
| 783 | const not = @fieldParentPtr(Node.NotExpression, "base", node); | |
| 784 | try writer.writeAll(" "); | |
| 785 | try writer.writeAll(not.not_token.slice(tree.source)); | |
| 786 | try writer.writeAll(" "); | |
| 787 | try writer.writeAll(not.number_token.slice(tree.source)); | |
| 788 | try writer.writeAll("\n"); | |
| 789 | }, | |
| 790 | .accelerators => { | |
| 791 | const accelerators = @fieldParentPtr(Node.Accelerators, "base", node); | |
| 792 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len }); | |
| 793 | for (accelerators.optional_statements) |statement| { | |
| 794 | try statement.dump(tree, writer, indent + 1); | |
| 795 | } | |
| 796 | try writer.writeByteNTimes(' ', indent); | |
| 797 | try writer.writeAll(accelerators.begin_token.slice(tree.source)); | |
| 798 | try writer.writeAll("\n"); | |
| 799 | for (accelerators.accelerators) |accelerator| { | |
| 800 | try accelerator.dump(tree, writer, indent + 1); | |
| 801 | } | |
| 802 | try writer.writeByteNTimes(' ', indent); | |
| 803 | try writer.writeAll(accelerators.end_token.slice(tree.source)); | |
| 804 | try writer.writeAll("\n"); | |
| 805 | }, | |
| 806 | .accelerator => { | |
| 807 | const accelerator = @fieldParentPtr(Node.Accelerator, "base", node); | |
| 808 | for (accelerator.type_and_options, 0..) |option, i| { | |
| 809 | if (i != 0) try writer.writeAll(","); | |
| 810 | try writer.writeByte(' '); | |
| 811 | try writer.writeAll(option.slice(tree.source)); | |
| 812 | } | |
| 813 | try writer.writeAll("\n"); | |
| 814 | try accelerator.event.dump(tree, writer, indent + 1); | |
| 815 | try accelerator.idvalue.dump(tree, writer, indent + 1); | |
| 816 | }, | |
| 817 | .dialog => { | |
| 818 | const dialog = @fieldParentPtr(Node.Dialog, "base", node); | |
| 819 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len }); | |
| 820 | inline for (.{ "x", "y", "width", "height" }) |arg| { | |
| 821 | try writer.writeByteNTimes(' ', indent + 1); | |
| 822 | try writer.writeAll(arg ++ ":\n"); | |
| 823 | try @field(dialog, arg).dump(tree, writer, indent + 2); | |
| 824 | } | |
| 825 | if (dialog.help_id) |help_id| { | |
| 826 | try writer.writeByteNTimes(' ', indent + 1); | |
| 827 | try writer.writeAll("help_id:\n"); | |
| 828 | try help_id.dump(tree, writer, indent + 2); | |
| 829 | } | |
| 830 | for (dialog.optional_statements) |statement| { | |
| 831 | try statement.dump(tree, writer, indent + 1); | |
| 832 | } | |
| 833 | try writer.writeByteNTimes(' ', indent); | |
| 834 | try writer.writeAll(dialog.begin_token.slice(tree.source)); | |
| 835 | try writer.writeAll("\n"); | |
| 836 | for (dialog.controls) |control| { | |
| 837 | try control.dump(tree, writer, indent + 1); | |
| 838 | } | |
| 839 | try writer.writeByteNTimes(' ', indent); | |
| 840 | try writer.writeAll(dialog.end_token.slice(tree.source)); | |
| 841 | try writer.writeAll("\n"); | |
| 842 | }, | |
| 843 | .control_statement => { | |
| 844 | const control = @fieldParentPtr(Node.ControlStatement, "base", node); | |
| 845 | try writer.print(" {s}", .{control.type.slice(tree.source)}); | |
| 846 | if (control.text) |text| { | |
| 847 | try writer.print(" text: {s}", .{text.slice(tree.source)}); | |
| 848 | } | |
| 849 | try writer.writeByte('\n'); | |
| 850 | if (control.class) |class| { | |
| 851 | try writer.writeByteNTimes(' ', indent + 1); | |
| 852 | try writer.writeAll("class:\n"); | |
| 853 | try class.dump(tree, writer, indent + 2); | |
| 854 | } | |
| 855 | inline for (.{ "id", "x", "y", "width", "height" }) |arg| { | |
| 856 | try writer.writeByteNTimes(' ', indent + 1); | |
| 857 | try writer.writeAll(arg ++ ":\n"); | |
| 858 | try @field(control, arg).dump(tree, writer, indent + 2); | |
| 859 | } | |
| 860 | inline for (.{ "style", "exstyle", "help_id" }) |arg| { | |
| 861 | if (@field(control, arg)) |val_node| { | |
| 862 | try writer.writeByteNTimes(' ', indent + 1); | |
| 863 | try writer.writeAll(arg ++ ":\n"); | |
| 864 | try val_node.dump(tree, writer, indent + 2); | |
| 865 | } | |
| 866 | } | |
| 867 | if (control.extra_data_begin != null) { | |
| 868 | try writer.writeByteNTimes(' ', indent); | |
| 869 | try writer.writeAll(control.extra_data_begin.?.slice(tree.source)); | |
| 870 | try writer.writeAll("\n"); | |
| 871 | for (control.extra_data) |data_node| { | |
| 872 | try data_node.dump(tree, writer, indent + 1); | |
| 873 | } | |
| 874 | try writer.writeByteNTimes(' ', indent); | |
| 875 | try writer.writeAll(control.extra_data_end.?.slice(tree.source)); | |
| 876 | try writer.writeAll("\n"); | |
| 877 | } | |
| 878 | }, | |
| 879 | .toolbar => { | |
| 880 | const toolbar = @fieldParentPtr(Node.Toolbar, "base", node); | |
| 881 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len }); | |
| 882 | inline for (.{ "button_width", "button_height" }) |arg| { | |
| 883 | try writer.writeByteNTimes(' ', indent + 1); | |
| 884 | try writer.writeAll(arg ++ ":\n"); | |
| 885 | try @field(toolbar, arg).dump(tree, writer, indent + 2); | |
| 886 | } | |
| 887 | try writer.writeByteNTimes(' ', indent); | |
| 888 | try writer.writeAll(toolbar.begin_token.slice(tree.source)); | |
| 889 | try writer.writeAll("\n"); | |
| 890 | for (toolbar.buttons) |button_or_sep| { | |
| 891 | try button_or_sep.dump(tree, writer, indent + 1); | |
| 892 | } | |
| 893 | try writer.writeByteNTimes(' ', indent); | |
| 894 | try writer.writeAll(toolbar.end_token.slice(tree.source)); | |
| 895 | try writer.writeAll("\n"); | |
| 896 | }, | |
| 897 | .menu => { | |
| 898 | const menu = @fieldParentPtr(Node.Menu, "base", node); | |
| 899 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len }); | |
| 900 | for (menu.optional_statements) |statement| { | |
| 901 | try statement.dump(tree, writer, indent + 1); | |
| 902 | } | |
| 903 | if (menu.help_id) |help_id| { | |
| 904 | try writer.writeByteNTimes(' ', indent + 1); | |
| 905 | try writer.writeAll("help_id:\n"); | |
| 906 | try help_id.dump(tree, writer, indent + 2); | |
| 907 | } | |
| 908 | try writer.writeByteNTimes(' ', indent); | |
| 909 | try writer.writeAll(menu.begin_token.slice(tree.source)); | |
| 910 | try writer.writeAll("\n"); | |
| 911 | for (menu.items) |item| { | |
| 912 | try item.dump(tree, writer, indent + 1); | |
| 913 | } | |
| 914 | try writer.writeByteNTimes(' ', indent); | |
| 915 | try writer.writeAll(menu.end_token.slice(tree.source)); | |
| 916 | try writer.writeAll("\n"); | |
| 917 | }, | |
| 918 | .menu_item => { | |
| 919 | const menu_item = @fieldParentPtr(Node.MenuItem, "base", node); | |
| 920 | try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len }); | |
| 921 | try menu_item.result.dump(tree, writer, indent + 1); | |
| 922 | }, | |
| 923 | .menu_item_separator => { | |
| 924 | const menu_item = @fieldParentPtr(Node.MenuItemSeparator, "base", node); | |
| 925 | try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) }); | |
| 926 | }, | |
| 927 | .menu_item_ex => { | |
| 928 | const menu_item = @fieldParentPtr(Node.MenuItemEx, "base", node); | |
| 929 | try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) }); | |
| 930 | inline for (.{ "id", "type", "state" }) |arg| { | |
| 931 | if (@field(menu_item, arg)) |val_node| { | |
| 932 | try writer.writeByteNTimes(' ', indent + 1); | |
| 933 | try writer.writeAll(arg ++ ":\n"); | |
| 934 | try val_node.dump(tree, writer, indent + 2); | |
| 935 | } | |
| 936 | } | |
| 937 | }, | |
| 938 | .popup => { | |
| 939 | const popup = @fieldParentPtr(Node.Popup, "base", node); | |
| 940 | try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len }); | |
| 941 | try writer.writeByteNTimes(' ', indent); | |
| 942 | try writer.writeAll(popup.begin_token.slice(tree.source)); | |
| 943 | try writer.writeAll("\n"); | |
| 944 | for (popup.items) |item| { | |
| 945 | try item.dump(tree, writer, indent + 1); | |
| 946 | } | |
| 947 | try writer.writeByteNTimes(' ', indent); | |
| 948 | try writer.writeAll(popup.end_token.slice(tree.source)); | |
| 949 | try writer.writeAll("\n"); | |
| 950 | }, | |
| 951 | .popup_ex => { | |
| 952 | const popup = @fieldParentPtr(Node.PopupEx, "base", node); | |
| 953 | try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) }); | |
| 954 | inline for (.{ "id", "type", "state", "help_id" }) |arg| { | |
| 955 | if (@field(popup, arg)) |val_node| { | |
| 956 | try writer.writeByteNTimes(' ', indent + 1); | |
| 957 | try writer.writeAll(arg ++ ":\n"); | |
| 958 | try val_node.dump(tree, writer, indent + 2); | |
| 959 | } | |
| 960 | } | |
| 961 | try writer.writeByteNTimes(' ', indent); | |
| 962 | try writer.writeAll(popup.begin_token.slice(tree.source)); | |
| 963 | try writer.writeAll("\n"); | |
| 964 | for (popup.items) |item| { | |
| 965 | try item.dump(tree, writer, indent + 1); | |
| 966 | } | |
| 967 | try writer.writeByteNTimes(' ', indent); | |
| 968 | try writer.writeAll(popup.end_token.slice(tree.source)); | |
| 969 | try writer.writeAll("\n"); | |
| 970 | }, | |
| 971 | .version_info => { | |
| 972 | const version_info = @fieldParentPtr(Node.VersionInfo, "base", node); | |
| 973 | try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len }); | |
| 974 | for (version_info.fixed_info) |fixed_info| { | |
| 975 | try fixed_info.dump(tree, writer, indent + 1); | |
| 976 | } | |
| 977 | try writer.writeByteNTimes(' ', indent); | |
| 978 | try writer.writeAll(version_info.begin_token.slice(tree.source)); | |
| 979 | try writer.writeAll("\n"); | |
| 980 | for (version_info.block_statements) |block| { | |
| 981 | try block.dump(tree, writer, indent + 1); | |
| 982 | } | |
| 983 | try writer.writeByteNTimes(' ', indent); | |
| 984 | try writer.writeAll(version_info.end_token.slice(tree.source)); | |
| 985 | try writer.writeAll("\n"); | |
| 986 | }, | |
| 987 | .version_statement => { | |
| 988 | const version_statement = @fieldParentPtr(Node.VersionStatement, "base", node); | |
| 989 | try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)}); | |
| 990 | for (version_statement.parts) |part| { | |
| 991 | try part.dump(tree, writer, indent + 1); | |
| 992 | } | |
| 993 | }, | |
| 994 | .block => { | |
| 995 | const block = @fieldParentPtr(Node.Block, "base", node); | |
| 996 | try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) }); | |
| 997 | for (block.values) |value| { | |
| 998 | try value.dump(tree, writer, indent + 1); | |
| 999 | } | |
| 1000 | try writer.writeByteNTimes(' ', indent); | |
| 1001 | try writer.writeAll(block.begin_token.slice(tree.source)); | |
| 1002 | try writer.writeAll("\n"); | |
| 1003 | for (block.children) |child| { | |
| 1004 | try child.dump(tree, writer, indent + 1); | |
| 1005 | } | |
| 1006 | try writer.writeByteNTimes(' ', indent); | |
| 1007 | try writer.writeAll(block.end_token.slice(tree.source)); | |
| 1008 | try writer.writeAll("\n"); | |
| 1009 | }, | |
| 1010 | .block_value => { | |
| 1011 | const block_value = @fieldParentPtr(Node.BlockValue, "base", node); | |
| 1012 | try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) }); | |
| 1013 | for (block_value.values) |value| { | |
| 1014 | try value.dump(tree, writer, indent + 1); | |
| 1015 | } | |
| 1016 | }, | |
| 1017 | .block_value_value => { | |
| 1018 | const block_value = @fieldParentPtr(Node.BlockValueValue, "base", node); | |
| 1019 | if (block_value.trailing_comma) { | |
| 1020 | try writer.writeAll(" ,"); | |
| 1021 | } | |
| 1022 | try writer.writeAll("\n"); | |
| 1023 | try block_value.expression.dump(tree, writer, indent + 1); | |
| 1024 | }, | |
| 1025 | .string_table => { | |
| 1026 | const string_table = @fieldParentPtr(Node.StringTable, "base", node); | |
| 1027 | try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len }); | |
| 1028 | for (string_table.optional_statements) |statement| { | |
| 1029 | try statement.dump(tree, writer, indent + 1); | |
| 1030 | } | |
| 1031 | try writer.writeByteNTimes(' ', indent); | |
| 1032 | try writer.writeAll(string_table.begin_token.slice(tree.source)); | |
| 1033 | try writer.writeAll("\n"); | |
| 1034 | for (string_table.strings) |string| { | |
| 1035 | try string.dump(tree, writer, indent + 1); | |
| 1036 | } | |
| 1037 | try writer.writeByteNTimes(' ', indent); | |
| 1038 | try writer.writeAll(string_table.end_token.slice(tree.source)); | |
| 1039 | try writer.writeAll("\n"); | |
| 1040 | }, | |
| 1041 | .string_table_string => { | |
| 1042 | try writer.writeAll("\n"); | |
| 1043 | const string = @fieldParentPtr(Node.StringTableString, "base", node); | |
| 1044 | try string.id.dump(tree, writer, indent + 1); | |
| 1045 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1046 | try writer.print("{s}\n", .{string.string.slice(tree.source)}); | |
| 1047 | }, | |
| 1048 | .language_statement => { | |
| 1049 | const language = @fieldParentPtr(Node.LanguageStatement, "base", node); | |
| 1050 | try writer.print(" {s}\n", .{language.language_token.slice(tree.source)}); | |
| 1051 | try language.primary_language_id.dump(tree, writer, indent + 1); | |
| 1052 | try language.sublanguage_id.dump(tree, writer, indent + 1); | |
| 1053 | }, | |
| 1054 | .font_statement => { | |
| 1055 | const font = @fieldParentPtr(Node.FontStatement, "base", node); | |
| 1056 | try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) }); | |
| 1057 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1058 | try writer.writeAll("point_size:\n"); | |
| 1059 | try font.point_size.dump(tree, writer, indent + 2); | |
| 1060 | inline for (.{ "weight", "italic", "char_set" }) |arg| { | |
| 1061 | if (@field(font, arg)) |arg_node| { | |
| 1062 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1063 | try writer.writeAll(arg ++ ":\n"); | |
| 1064 | try arg_node.dump(tree, writer, indent + 2); | |
| 1065 | } | |
| 1066 | } | |
| 1067 | }, | |
| 1068 | .simple_statement => { | |
| 1069 | const statement = @fieldParentPtr(Node.SimpleStatement, "base", node); | |
| 1070 | try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)}); | |
| 1071 | try statement.value.dump(tree, writer, indent + 1); | |
| 1072 | }, | |
| 1073 | .invalid => { | |
| 1074 | const invalid = @fieldParentPtr(Node.Invalid, "base", node); | |
| 1075 | try writer.print(" context.len: {}\n", .{invalid.context.len}); | |
| 1076 | for (invalid.context) |context_token| { | |
| 1077 | try writer.writeByteNTimes(' ', indent + 1); | |
| 1078 | try writer.print("{s}:{s}", .{ @tagName(context_token.id), context_token.slice(tree.source) }); | |
| 1079 | try writer.writeByte('\n'); | |
| 1080 | } | |
| 1081 | }, | |
| 1082 | } | |
| 1083 | } | |
| 1084 | }; |
src/resinator/bmp.zig 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 | ||
| 21 | const std = @import("std"); | |
| 22 | const BitmapHeader = @import("ico.zig").BitmapHeader; | |
| 23 | const builtin = @import("builtin"); | |
| 24 | const native_endian = builtin.cpu.arch.endian(); | |
| 25 | ||
| 26 | pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian); | |
| 27 | pub const file_header_len = 14; | |
| 28 | ||
| 29 | pub const ReadError = error{ | |
| 30 | UnexpectedEOF, | |
| 31 | InvalidFileHeader, | |
| 32 | ImpossiblePixelDataOffset, | |
| 33 | UnknownBitmapVersion, | |
| 34 | InvalidBitsPerPixel, | |
| 35 | TooManyColorsInPalette, | |
| 36 | MissingBitfieldMasks, | |
| 37 | }; | |
| 38 | ||
| 39 | pub 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 | ||
| 90 | pub 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 | |
| 145 | pub 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 | |
| 154 | pub 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 | ||
| 212 | pub 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 | ||
| 226 | fn 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 | ||
| 232 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const CodePage = @import("code_pages.zig").CodePage; | |
| 3 | const lang = @import("lang.zig"); | |
| 4 | const res = @import("res.zig"); | |
| 5 | const Allocator = std.mem.Allocator; | |
| 6 | const lex = @import("lex.zig"); | |
| 7 | ||
| 8 | /// This is what /SL 100 will set the maximum string literal length to | |
| 9 | pub const max_string_literal_length_100_percent = 8192; | |
| 10 | ||
| 11 | pub const usage_string_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 | ||
| 60 | pub 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 | ||
| 66 | pub 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 | ||
| 125 | pub 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 | ||
| 296 | pub 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 | ||
| 386 | pub 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. | |
| 390 | pub 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 | |
| 903 | pub 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 | |
| 918 | pub 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 | ||
| 960 | test 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 | ||
| 973 | pub 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 | ||
| 1079 | fn testParse(args: []const []const u8) !Options { | |
| 1080 | return (try testParseOutput(args, "")).?; | |
| 1081 | } | |
| 1082 | ||
| 1083 | fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options { | |
| 1084 | return (try testParseOutput(args, expected_output)).?; | |
| 1085 | } | |
| 1086 | ||
| 1087 | fn 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 | ||
| 1096 | fn 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 | ||
| 1118 | test "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 | ||
| 1204 | test "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 | ||
| 1219 | test "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 | ||
| 1257 | test "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 | ||
| 1316 | test "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 | ||
| 1359 | test "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 | ||
| 1380 | test "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 | ||
| 1419 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const windows1252 = @import("windows1252.zig"); | |
| 3 | ||
| 4 | // TODO: Parts of this comment block may be more relevant to string/NameOrOrdinal parsing | |
| 5 | // than it is to the stuff in this file. | |
| 6 | // | |
| 7 | // ‰ representations for context: | |
| 8 | // Win-1252 89 | |
| 9 | // UTF-8 E2 80 B0 | |
| 10 | // UTF-16 20 30 | |
| 11 | // | |
| 12 | // With code page 65001: | |
| 13 | // ‰ RCDATA { "‰" L"‰" } | |
| 14 | // File encoded as Windows-1252: | |
| 15 | // ‰ => <U+FFFD REPLACEMENT CHARACTER> as u16 | |
| 16 | // "‰" => 0x3F ('?') | |
| 17 | // L"‰" => <U+FFFD REPLACEMENT CHARACTER> as u16 | |
| 18 | // File encoded as UTF-8: | |
| 19 | // ‰ => <U+2030 ‰> as u16 | |
| 20 | // "‰" => 0x89 ('‰' encoded as Windows-1252) | |
| 21 | // L"‰" => <U+2030 ‰> as u16 | |
| 22 | // | |
| 23 | // With code page 1252: | |
| 24 | // ‰ RCDATA { "‰" L"‰" } | |
| 25 | // File encoded as Windows-1252: | |
| 26 | // ‰ => <U+2030 ‰> as u16 | |
| 27 | // "‰" => 0x89 ('‰' encoded as Windows-1252) | |
| 28 | // L"‰" => <U+2030 ‰> as u16 | |
| 29 | // File encoded as UTF-8: | |
| 30 | // ‰ => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 | |
| 31 | // ^ first byte of utf8 representation | |
| 32 | // ^ second byte of UTF-8 representation (0x80), but interpretted as | |
| 33 | // Windows-1252 ('€') and then converted to UTF-16 (<U+20AC>) | |
| 34 | // ^ third byte of utf8 representation | |
| 35 | // "‰" => 0xE2, 0x80, 0xB0 (the bytes of the UTF-8 representation) | |
| 36 | // L"‰" => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 (see '‰ =>' explanation) | |
| 37 | // | |
| 38 | // With code page 1252: | |
| 39 | // <0x90> RCDATA { "<0x90>" L"<0x90>" } | |
| 40 | // File encoded as Windows-1252: | |
| 41 | // <0x90> => 0x90 as u16 | |
| 42 | // "<0x90>" => 0x90 | |
| 43 | // L"<0x90>" => 0x90 as u16 | |
| 44 | // File encoded as UTF-8: | |
| 45 | // <0x90> => 0xC2 as u16, 0x90 as u16 | |
| 46 | // "<0x90>" => 0xC2, 0x90 (the bytes of the UTF-8 representation of <U+0090>) | |
| 47 | // L"<0x90>" => 0xC2 as u16, 0x90 as u16 | |
| 48 | // | |
| 49 | // Within a raw data block, file encoded as Windows-1252 (Â is <0xC2>): | |
| 50 | // "Âa" L"Âa" "\xC2ad" L"\xC2AD" | |
| 51 | // With code page 1252: | |
| 52 | // C2 61 C2 00 61 00 C2 61 64 AD C2 | |
| 53 | // Â^ a^ Â~~~^ a~~~^ .^ a^ d^ ^~~~~\xC2AD | |
| 54 | // \xC2~` | |
| 55 | // With code page 65001: | |
| 56 | // 3F 61 FD FF 61 00 C2 61 64 AD C2 | |
| 57 | // ^. a^ ^~~~. a~~~^ ^. a^ d^ ^~~~~\xC2AD | |
| 58 | // `. `. `~\xC2 | |
| 59 | // `. `.~<0xC2>a is not well-formed UTF-8 (0xC2 expects a continutation byte after it). | |
| 60 | // `. Because 'a' is a valid first byte of a UTF-8 sequence, it is not included in the | |
| 61 | // `. invalid sequence so only the <0xC2> gets converted to <U+FFFD>. | |
| 62 | // `~Same as ^ but converted to '?' instead. | |
| 63 | // | |
| 64 | // Within a raw data block, file encoded as Windows-1252 (ð is <0xF0>, € is <0x80>): | |
| 65 | // "ð€a" L"ð€a" | |
| 66 | // With code page 1252: | |
| 67 | // F0 80 61 F0 00 AC 20 61 00 | |
| 68 | // ð^ €^ a^ ð~~~^ €~~~^ a~~~^ | |
| 69 | // With code page 65001: | |
| 70 | // 3F 61 FD FF 61 00 | |
| 71 | // ^. a^ ^~~~. a~~~^ | |
| 72 | // `. `. | |
| 73 | // `. `.~<0xF0><0x80> is not well-formed UTF-8, and <0x80> is not a valid first byte, so | |
| 74 | // `. both bytes are considered an invalid sequence and get converted to '<U+FFFD>' | |
| 75 | // `~Same as ^ but converted to '?' instead. | |
| 76 | ||
| 77 | /// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers | |
| 78 | pub const CodePage = enum(u16) { | |
| 79 | // supported | |
| 80 | windows1252 = 1252, // windows-1252 ANSI Latin 1; Western European (Windows) | |
| 81 | utf8 = 65001, // utf-8 Unicode (UTF-8) | |
| 82 | ||
| 83 | // unsupported but valid | |
| 84 | ibm037 = 37, // IBM037 IBM EBCDIC US-Canada | |
| 85 | ibm437 = 437, // IBM437 OEM United States | |
| 86 | ibm500 = 500, // IBM500 IBM EBCDIC International | |
| 87 | asmo708 = 708, // ASMO-708 Arabic (ASMO 708) | |
| 88 | asmo449plus = 709, // Arabic (ASMO-449+, BCON V4) | |
| 89 | transparent_arabic = 710, // Arabic - Transparent Arabic | |
| 90 | dos720 = 720, // DOS-720 Arabic (Transparent ASMO); Arabic (DOS) | |
| 91 | ibm737 = 737, // ibm737 OEM Greek (formerly 437G); Greek (DOS) | |
| 92 | ibm775 = 775, // ibm775 OEM Baltic; Baltic (DOS) | |
| 93 | ibm850 = 850, // ibm850 OEM Multilingual Latin 1; Western European (DOS) | |
| 94 | ibm852 = 852, // ibm852 OEM Latin 2; Central European (DOS) | |
| 95 | ibm855 = 855, // IBM855 OEM Cyrillic (primarily Russian) | |
| 96 | ibm857 = 857, // ibm857 OEM Turkish; Turkish (DOS) | |
| 97 | ibm00858 = 858, // IBM00858 OEM Multilingual Latin 1 + Euro symbol | |
| 98 | ibm860 = 860, // IBM860 OEM Portuguese; Portuguese (DOS) | |
| 99 | ibm861 = 861, // ibm861 OEM Icelandic; Icelandic (DOS) | |
| 100 | dos862 = 862, // DOS-862 OEM Hebrew; Hebrew (DOS) | |
| 101 | ibm863 = 863, // IBM863 OEM French Canadian; French Canadian (DOS) | |
| 102 | ibm864 = 864, // IBM864 OEM Arabic; Arabic (864) | |
| 103 | ibm865 = 865, // IBM865 OEM Nordic; Nordic (DOS) | |
| 104 | cp866 = 866, // cp866 OEM Russian; Cyrillic (DOS) | |
| 105 | ibm869 = 869, // ibm869 OEM Modern Greek; Greek, Modern (DOS) | |
| 106 | ibm870 = 870, // IBM870 IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2 | |
| 107 | windows874 = 874, // windows-874 Thai (Windows) | |
| 108 | cp875 = 875, // cp875 IBM EBCDIC Greek Modern | |
| 109 | shift_jis = 932, // shift_jis ANSI/OEM Japanese; Japanese (Shift-JIS) | |
| 110 | gb2312 = 936, // gb2312 ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312) | |
| 111 | ks_c_5601_1987 = 949, // ks_c_5601-1987 ANSI/OEM Korean (Unified Hangul Code) | |
| 112 | big5 = 950, // big5 ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5) | |
| 113 | ibm1026 = 1026, // IBM1026 IBM EBCDIC Turkish (Latin 5) | |
| 114 | ibm01047 = 1047, // IBM01047 IBM EBCDIC Latin 1/Open System | |
| 115 | ibm01140 = 1140, // IBM01140 IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro) | |
| 116 | ibm01141 = 1141, // IBM01141 IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro) | |
| 117 | ibm01142 = 1142, // IBM01142 IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro) | |
| 118 | ibm01143 = 1143, // IBM01143 IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro) | |
| 119 | ibm01144 = 1144, // IBM01144 IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro) | |
| 120 | ibm01145 = 1145, // IBM01145 IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro) | |
| 121 | ibm01146 = 1146, // IBM01146 IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro) | |
| 122 | ibm01147 = 1147, // IBM01147 IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro) | |
| 123 | ibm01148 = 1148, // IBM01148 IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro) | |
| 124 | ibm01149 = 1149, // IBM01149 IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro) | |
| 125 | utf16 = 1200, // utf-16 Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications | |
| 126 | utf16_fffe = 1201, // unicodeFFFE Unicode UTF-16, big endian byte order; available only to managed applications | |
| 127 | windows1250 = 1250, // windows-1250 ANSI Central European; Central European (Windows) | |
| 128 | windows1251 = 1251, // windows-1251 ANSI Cyrillic; Cyrillic (Windows) | |
| 129 | windows1253 = 1253, // windows-1253 ANSI Greek; Greek (Windows) | |
| 130 | windows1254 = 1254, // windows-1254 ANSI Turkish; Turkish (Windows) | |
| 131 | windows1255 = 1255, // windows-1255 ANSI Hebrew; Hebrew (Windows) | |
| 132 | windows1256 = 1256, // windows-1256 ANSI Arabic; Arabic (Windows) | |
| 133 | windows1257 = 1257, // windows-1257 ANSI Baltic; Baltic (Windows) | |
| 134 | windows1258 = 1258, // windows-1258 ANSI/OEM Vietnamese; Vietnamese (Windows) | |
| 135 | johab = 1361, // Johab Korean (Johab) | |
| 136 | macintosh = 10000, // macintosh MAC Roman; Western European (Mac) | |
| 137 | x_mac_japanese = 10001, // x-mac-japanese Japanese (Mac) | |
| 138 | x_mac_chinesetrad = 10002, // x-mac-chinesetrad MAC Traditional Chinese (Big5); Chinese Traditional (Mac) | |
| 139 | x_mac_korean = 10003, // x-mac-korean Korean (Mac) | |
| 140 | x_mac_arabic = 10004, // x-mac-arabic Arabic (Mac) | |
| 141 | x_mac_hebrew = 10005, // x-mac-hebrew Hebrew (Mac) | |
| 142 | x_mac_greek = 10006, // x-mac-greek Greek (Mac) | |
| 143 | x_mac_cyrillic = 10007, // x-mac-cyrillic Cyrillic (Mac) | |
| 144 | x_mac_chinesesimp = 10008, // x-mac-chinesesimp MAC Simplified Chinese (GB 2312); Chinese Simplified (Mac) | |
| 145 | x_mac_romanian = 10010, // x-mac-romanian Romanian (Mac) | |
| 146 | x_mac_ukranian = 10017, // x-mac-ukrainian Ukrainian (Mac) | |
| 147 | x_mac_thai = 10021, // x-mac-thai Thai (Mac) | |
| 148 | x_mac_ce = 10029, // x-mac-ce MAC Latin 2; Central European (Mac) | |
| 149 | x_mac_icelandic = 10079, // x-mac-icelandic Icelandic (Mac) | |
| 150 | x_mac_turkish = 10081, // x-mac-turkish Turkish (Mac) | |
| 151 | x_mac_croatian = 10082, // x-mac-croatian Croatian (Mac) | |
| 152 | utf32 = 12000, // utf-32 Unicode UTF-32, little endian byte order; available only to managed applications | |
| 153 | utf32_be = 12001, // utf-32BE Unicode UTF-32, big endian byte order; available only to managed applications | |
| 154 | x_chinese_cns = 20000, // x-Chinese_CNS CNS Taiwan; Chinese Traditional (CNS) | |
| 155 | x_cp20001 = 20001, // x-cp20001 TCA Taiwan | |
| 156 | x_chinese_eten = 20002, // x_Chinese-Eten Eten Taiwan; Chinese Traditional (Eten) | |
| 157 | x_cp20003 = 20003, // x-cp20003 IBM5550 Taiwan | |
| 158 | x_cp20004 = 20004, // x-cp20004 TeleText Taiwan | |
| 159 | x_cp20005 = 20005, // x-cp20005 Wang Taiwan | |
| 160 | x_ia5 = 20105, // x-IA5 IA5 (IRV International Alphabet No. 5, 7-bit); Western European (IA5) | |
| 161 | x_ia5_german = 20106, // x-IA5-German IA5 German (7-bit) | |
| 162 | x_ia5_swedish = 20107, // x-IA5-Swedish IA5 Swedish (7-bit) | |
| 163 | x_ia5_norwegian = 20108, // x-IA5-Norwegian IA5 Norwegian (7-bit) | |
| 164 | us_ascii = 20127, // us-ascii US-ASCII (7-bit) | |
| 165 | x_cp20261 = 20261, // x-cp20261 T.61 | |
| 166 | x_cp20269 = 20269, // x-cp20269 ISO 6937 Non-Spacing Accent | |
| 167 | ibm273 = 20273, // IBM273 IBM EBCDIC Germany | |
| 168 | ibm277 = 20277, // IBM277 IBM EBCDIC Denmark-Norway | |
| 169 | ibm278 = 20278, // IBM278 IBM EBCDIC Finland-Sweden | |
| 170 | ibm280 = 20280, // IBM280 IBM EBCDIC Italy | |
| 171 | ibm284 = 20284, // IBM284 IBM EBCDIC Latin America-Spain | |
| 172 | ibm285 = 20285, // IBM285 IBM EBCDIC United Kingdom | |
| 173 | ibm290 = 20290, // IBM290 IBM EBCDIC Japanese Katakana Extended | |
| 174 | ibm297 = 20297, // IBM297 IBM EBCDIC France | |
| 175 | ibm420 = 20420, // IBM420 IBM EBCDIC Arabic | |
| 176 | ibm423 = 20423, // IBM423 IBM EBCDIC Greek | |
| 177 | ibm424 = 20424, // IBM424 IBM EBCDIC Hebrew | |
| 178 | x_ebcdic_korean_extended = 20833, // x-EBCDIC-KoreanExtended IBM EBCDIC Korean Extended | |
| 179 | ibm_thai = 20838, // IBM-Thai IBM EBCDIC Thai | |
| 180 | koi8_r = 20866, // koi8-r Russian (KOI8-R); Cyrillic (KOI8-R) | |
| 181 | ibm871 = 20871, // IBM871 IBM EBCDIC Icelandic | |
| 182 | ibm880 = 20880, // IBM880 IBM EBCDIC Cyrillic Russian | |
| 183 | ibm905 = 20905, // IBM905 IBM EBCDIC Turkish | |
| 184 | ibm00924 = 20924, // IBM00924 IBM EBCDIC Latin 1/Open System (1047 + Euro symbol) | |
| 185 | euc_jp_jis = 20932, // EUC-JP Japanese (JIS 0208-1990 and 0212-1990) | |
| 186 | x_cp20936 = 20936, // x-cp20936 Simplified Chinese (GB2312); Chinese Simplified (GB2312-80) | |
| 187 | x_cp20949 = 20949, // x-cp20949 Korean Wansung | |
| 188 | cp1025 = 21025, // cp1025 IBM EBCDIC Cyrillic Serbian-Bulgarian | |
| 189 | // = 21027, // (deprecated) | |
| 190 | koi8_u = 21866, // koi8-u Ukrainian (KOI8-U); Cyrillic (KOI8-U) | |
| 191 | iso8859_1 = 28591, // iso-8859-1 ISO 8859-1 Latin 1; Western European (ISO) | |
| 192 | iso8859_2 = 28592, // iso-8859-2 ISO 8859-2 Central European; Central European (ISO) | |
| 193 | iso8859_3 = 28593, // iso-8859-3 ISO 8859-3 Latin 3 | |
| 194 | iso8859_4 = 28594, // iso-8859-4 ISO 8859-4 Baltic | |
| 195 | iso8859_5 = 28595, // iso-8859-5 ISO 8859-5 Cyrillic | |
| 196 | iso8859_6 = 28596, // iso-8859-6 ISO 8859-6 Arabic | |
| 197 | iso8859_7 = 28597, // iso-8859-7 ISO 8859-7 Greek | |
| 198 | iso8859_8 = 28598, // iso-8859-8 ISO 8859-8 Hebrew; Hebrew (ISO-Visual) | |
| 199 | iso8859_9 = 28599, // iso-8859-9 ISO 8859-9 Turkish | |
| 200 | iso8859_13 = 28603, // iso-8859-13 ISO 8859-13 Estonian | |
| 201 | iso8859_15 = 28605, // iso-8859-15 ISO 8859-15 Latin 9 | |
| 202 | x_europa = 29001, // x-Europa Europa 3 | |
| 203 | is8859_8_i = 38598, // iso-8859-8-i ISO 8859-8 Hebrew; Hebrew (ISO-Logical) | |
| 204 | iso2022_jp = 50220, // iso-2022-jp ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS) | |
| 205 | cs_iso2022_jp = 50221, // csISO2022JP ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana) | |
| 206 | iso2022_jp_jis_x = 50222, // iso-2022-jp ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI) | |
| 207 | iso2022_kr = 50225, // iso-2022-kr ISO 2022 Korean | |
| 208 | x_cp50227 = 50227, // x-cp50227 ISO 2022 Simplified Chinese; Chinese Simplified (ISO 2022) | |
| 209 | iso2022_chinesetrad = 50229, // ISO 2022 Traditional Chinese | |
| 210 | ebcdic_jp_katakana_extended = 50930, // EBCDIC Japanese (Katakana) Extended | |
| 211 | ebcdic_us_ca_jp = 50931, // EBCDIC US-Canada and Japanese | |
| 212 | ebcdic_kr_extended = 50933, // EBCDIC Korean Extended and Korean | |
| 213 | ebcdic_chinesesimp_extended = 50935, // EBCDIC Simplified Chinese Extended and Simplified Chinese | |
| 214 | ebcdic_chinesesimp = 50936, // EBCDIC Simplified Chinese | |
| 215 | ebcdic_us_ca_chinesetrad = 50937, // EBCDIC US-Canada and Traditional Chinese | |
| 216 | ebcdic_jp_latin_extended = 50939, // EBCDIC Japanese (Latin) Extended and Japanese | |
| 217 | euc_jp = 51932, // euc-jp EUC Japanese | |
| 218 | euc_cn = 51936, // EUC-CN EUC Simplified Chinese; Chinese Simplified (EUC) | |
| 219 | euc_kr = 51949, // euc-kr EUC Korean | |
| 220 | euc_chinesetrad = 51950, // EUC Traditional Chinese | |
| 221 | hz_gb2312 = 52936, // hz-gb-2312 HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ) | |
| 222 | gb18030 = 54936, // GB18030 Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030) | |
| 223 | x_iscii_de = 57002, // x-iscii-de ISCII Devanagari | |
| 224 | x_iscii_be = 57003, // x-iscii-be ISCII Bangla | |
| 225 | x_iscii_ta = 57004, // x-iscii-ta ISCII Tamil | |
| 226 | x_iscii_te = 57005, // x-iscii-te ISCII Telugu | |
| 227 | x_iscii_as = 57006, // x-iscii-as ISCII Assamese | |
| 228 | x_iscii_or = 57007, // x-iscii-or ISCII Odia | |
| 229 | x_iscii_ka = 57008, // x-iscii-ka ISCII Kannada | |
| 230 | x_iscii_ma = 57009, // x-iscii-ma ISCII Malayalam | |
| 231 | x_iscii_gu = 57010, // x-iscii-gu ISCII Gujarati | |
| 232 | x_iscii_pa = 57011, // x-iscii-pa ISCII Punjabi | |
| 233 | utf7 = 65000, // utf-7 Unicode (UTF-7) | |
| 234 | ||
| 235 | pub fn codepointAt(code_page: CodePage, index: usize, bytes: []const u8) ?Codepoint { | |
| 236 | if (index >= bytes.len) return null; | |
| 237 | switch (code_page) { | |
| 238 | .windows1252 => { | |
| 239 | // All byte values have a representation, so just convert the byte | |
| 240 | return Codepoint{ | |
| 241 | .value = windows1252.toCodepoint(bytes[index]), | |
| 242 | .byte_len = 1, | |
| 243 | }; | |
| 244 | }, | |
| 245 | .utf8 => { | |
| 246 | return Utf8.WellFormedDecoder.decode(bytes[index..]); | |
| 247 | }, | |
| 248 | else => unreachable, | |
| 249 | } | |
| 250 | } | |
| 251 | ||
| 252 | pub fn isSupported(code_page: CodePage) bool { | |
| 253 | return switch (code_page) { | |
| 254 | .windows1252, .utf8 => true, | |
| 255 | else => false, | |
| 256 | }; | |
| 257 | } | |
| 258 | ||
| 259 | pub fn getByIdentifier(identifier: u16) !CodePage { | |
| 260 | // There's probably a more efficient way to do this (e.g. ComptimeHashMap?) but | |
| 261 | // this should be fine, especially since this function likely won't be called much. | |
| 262 | inline for (@typeInfo(CodePage).Enum.fields) |enumField| { | |
| 263 | if (identifier == enumField.value) { | |
| 264 | return @field(CodePage, enumField.name); | |
| 265 | } | |
| 266 | } | |
| 267 | return error.InvalidCodePage; | |
| 268 | } | |
| 269 | ||
| 270 | pub fn getByIdentifierEnsureSupported(identifier: u16) !CodePage { | |
| 271 | const code_page = try getByIdentifier(identifier); | |
| 272 | switch (isSupported(code_page)) { | |
| 273 | true => return code_page, | |
| 274 | false => return error.UnsupportedCodePage, | |
| 275 | } | |
| 276 | } | |
| 277 | }; | |
| 278 | ||
| 279 | pub const Utf8 = struct { | |
| 280 | /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section | |
| 281 | /// D92 of Chapter 3 of the Unicode standard (Table 3-7 specifically). | |
| 282 | pub const WellFormedDecoder = struct { | |
| 283 | /// Like std.unicode.utf8ByteSequenceLength, but: | |
| 284 | /// - Rejects non-well-formed first bytes, i.e. C0-C1, F5-FF | |
| 285 | /// - Returns an optional value instead of an error union | |
| 286 | pub fn sequenceLength(first_byte: u8) ?u3 { | |
| 287 | return switch (first_byte) { | |
| 288 | 0x00...0x7F => 1, | |
| 289 | 0xC2...0xDF => 2, | |
| 290 | 0xE0...0xEF => 3, | |
| 291 | 0xF0...0xF4 => 4, | |
| 292 | else => null, | |
| 293 | }; | |
| 294 | } | |
| 295 | ||
| 296 | fn isContinuationByte(byte: u8) bool { | |
| 297 | return switch (byte) { | |
| 298 | 0x80...0xBF => true, | |
| 299 | else => false, | |
| 300 | }; | |
| 301 | } | |
| 302 | ||
| 303 | pub fn decode(bytes: []const u8) Codepoint { | |
| 304 | std.debug.assert(bytes.len > 0); | |
| 305 | 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 | ||
| 368 | test "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 | ||
| 375 | test "codepointAt invalid utf8" { | |
| 376 | { | |
| 377 | const invalid_utf8 = "\xf0\xf0\x80\x80\x80"; | |
| 378 | try std.testing.expectEqual(Codepoint{ | |
| 379 | .value = Codepoint.invalid, | |
| 380 | .byte_len = 1, | |
| 381 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 382 | try std.testing.expectEqual(Codepoint{ | |
| 383 | .value = Codepoint.invalid, | |
| 384 | .byte_len = 2, | |
| 385 | }, CodePage.utf8.codepointAt(1, invalid_utf8).?); | |
| 386 | try std.testing.expectEqual(Codepoint{ | |
| 387 | .value = Codepoint.invalid, | |
| 388 | .byte_len = 1, | |
| 389 | }, CodePage.utf8.codepointAt(3, invalid_utf8).?); | |
| 390 | try std.testing.expectEqual(Codepoint{ | |
| 391 | .value = Codepoint.invalid, | |
| 392 | .byte_len = 1, | |
| 393 | }, CodePage.utf8.codepointAt(4, invalid_utf8).?); | |
| 394 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(5, invalid_utf8)); | |
| 395 | } | |
| 396 | ||
| 397 | { | |
| 398 | const invalid_utf8 = "\xE1\xA0\xC0"; | |
| 399 | try std.testing.expectEqual(Codepoint{ | |
| 400 | .value = Codepoint.invalid, | |
| 401 | .byte_len = 2, | |
| 402 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 403 | try std.testing.expectEqual(Codepoint{ | |
| 404 | .value = Codepoint.invalid, | |
| 405 | .byte_len = 1, | |
| 406 | }, CodePage.utf8.codepointAt(2, invalid_utf8).?); | |
| 407 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(3, invalid_utf8)); | |
| 408 | } | |
| 409 | ||
| 410 | { | |
| 411 | const invalid_utf8 = "\xD2"; | |
| 412 | try std.testing.expectEqual(Codepoint{ | |
| 413 | .value = Codepoint.invalid, | |
| 414 | .byte_len = 1, | |
| 415 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 416 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, invalid_utf8)); | |
| 417 | } | |
| 418 | ||
| 419 | { | |
| 420 | const invalid_utf8 = "\xE1\xA0"; | |
| 421 | try std.testing.expectEqual(Codepoint{ | |
| 422 | .value = Codepoint.invalid, | |
| 423 | .byte_len = 2, | |
| 424 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 425 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8)); | |
| 426 | } | |
| 427 | ||
| 428 | { | |
| 429 | const invalid_utf8 = "\xC5\xFF"; | |
| 430 | try std.testing.expectEqual(Codepoint{ | |
| 431 | .value = Codepoint.invalid, | |
| 432 | .byte_len = 1, | |
| 433 | }, CodePage.utf8.codepointAt(0, invalid_utf8).?); | |
| 434 | try std.testing.expectEqual(Codepoint{ | |
| 435 | .value = Codepoint.invalid, | |
| 436 | .byte_len = 1, | |
| 437 | }, CodePage.utf8.codepointAt(1, invalid_utf8).?); | |
| 438 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8)); | |
| 439 | } | |
| 440 | } | |
| 441 | ||
| 442 | test "codepointAt utf8 encoded" { | |
| 443 | const utf8_encoded = "²"; | |
| 444 | ||
| 445 | // with code page utf8 | |
| 446 | try std.testing.expectEqual(Codepoint{ | |
| 447 | .value = '²', | |
| 448 | .byte_len = 2, | |
| 449 | }, CodePage.utf8.codepointAt(0, utf8_encoded).?); | |
| 450 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, utf8_encoded)); | |
| 451 | ||
| 452 | // with code page windows1252 | |
| 453 | try std.testing.expectEqual(Codepoint{ | |
| 454 | .value = '\xC2', | |
| 455 | .byte_len = 1, | |
| 456 | }, CodePage.windows1252.codepointAt(0, utf8_encoded).?); | |
| 457 | try std.testing.expectEqual(Codepoint{ | |
| 458 | .value = '\xB2', | |
| 459 | .byte_len = 1, | |
| 460 | }, CodePage.windows1252.codepointAt(1, utf8_encoded).?); | |
| 461 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, utf8_encoded)); | |
| 462 | } | |
| 463 | ||
| 464 | test "codepointAt windows1252 encoded" { | |
| 465 | const windows1252_encoded = "\xB2"; | |
| 466 | ||
| 467 | // with code page utf8 | |
| 468 | try std.testing.expectEqual(Codepoint{ | |
| 469 | .value = Codepoint.invalid, | |
| 470 | .byte_len = 1, | |
| 471 | }, CodePage.utf8.codepointAt(0, windows1252_encoded).?); | |
| 472 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, windows1252_encoded)); | |
| 473 | ||
| 474 | // with code page windows1252 | |
| 475 | try std.testing.expectEqual(Codepoint{ | |
| 476 | .value = '\xB2', | |
| 477 | .byte_len = 1, | |
| 478 | }, CodePage.windows1252.codepointAt(0, windows1252_encoded).?); | |
| 479 | try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, windows1252_encoded)); | |
| 480 | } | |
| 481 | ||
| 482 | pub const Codepoint = struct { | |
| 483 | value: u21, | |
| 484 | byte_len: usize, | |
| 485 | ||
| 486 | pub const invalid: u21 = std.math.maxInt(u21); | |
| 487 | }; |
src/resinator/comments.zig 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 | ||
| 16 | const std = @import("std"); | |
| 17 | const Allocator = std.mem.Allocator; | |
| 18 | const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter; | |
| 19 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 20 | const LineHandler = @import("lex.zig").LineHandler; | |
| 21 | const formsLineEndingPair = @import("source_mapping.zig").formsLineEndingPair; | |
| 22 | ||
| 23 | /// `buf` must be at least as long as `source` | |
| 24 | /// In-place transformation is supported (i.e. `source` and `buf` can be the same slice) | |
| 25 | pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMappings) []u8 { | |
| 26 | std.debug.assert(buf.len >= source.len); | |
| 27 | var result = UncheckedSliceWriter{ .slice = buf }; | |
| 28 | const State = enum { | |
| 29 | start, | |
| 30 | forward_slash, | |
| 31 | line_comment, | |
| 32 | multiline_comment, | |
| 33 | multiline_comment_end, | |
| 34 | single_quoted, | |
| 35 | single_quoted_escape, | |
| 36 | double_quoted, | |
| 37 | double_quoted_escape, | |
| 38 | }; | |
| 39 | var state: State = .start; | |
| 40 | var index: usize = 0; | |
| 41 | var pending_start: ?usize = null; | |
| 42 | var line_handler = LineHandler{ .buffer = source }; | |
| 43 | while (index < source.len) : (index += 1) { | |
| 44 | const c = source[index]; | |
| 45 | // TODO: Disallow \x1A, \x00, \x7F in comments. At least \x1A and \x00 can definitely | |
| 46 | // cause errors or parsing weirdness in the Win32 RC compiler. These are disallowed | |
| 47 | // in the lexer, but comments are stripped before getting to the lexer. | |
| 48 | switch (state) { | |
| 49 | .start => switch (c) { | |
| 50 | '/' => { | |
| 51 | state = .forward_slash; | |
| 52 | pending_start = index; | |
| 53 | }, | |
| 54 | '\r', '\n' => { | |
| 55 | _ = line_handler.incrementLineNumber(index); | |
| 56 | result.write(c); | |
| 57 | }, | |
| 58 | else => { | |
| 59 | switch (c) { | |
| 60 | '"' => state = .double_quoted, | |
| 61 | '\'' => state = .single_quoted, | |
| 62 | else => {}, | |
| 63 | } | |
| 64 | result.write(c); | |
| 65 | }, | |
| 66 | }, | |
| 67 | .forward_slash => switch (c) { | |
| 68 | '/' => state = .line_comment, | |
| 69 | '*' => { | |
| 70 | state = .multiline_comment; | |
| 71 | }, | |
| 72 | else => { | |
| 73 | _ = line_handler.maybeIncrementLineNumber(index); | |
| 74 | result.writeSlice(source[pending_start.? .. index + 1]); | |
| 75 | pending_start = null; | |
| 76 | state = .start; | |
| 77 | }, | |
| 78 | }, | |
| 79 | .line_comment => switch (c) { | |
| 80 | '\r', '\n' => { | |
| 81 | _ = line_handler.incrementLineNumber(index); | |
| 82 | result.write(c); | |
| 83 | state = .start; | |
| 84 | }, | |
| 85 | else => {}, | |
| 86 | }, | |
| 87 | .multiline_comment => switch (c) { | |
| 88 | '\r' => handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings), | |
| 89 | '\n' => { | |
| 90 | _ = line_handler.incrementLineNumber(index); | |
| 91 | result.write(c); | |
| 92 | }, | |
| 93 | '*' => state = .multiline_comment_end, | |
| 94 | else => {}, | |
| 95 | }, | |
| 96 | .multiline_comment_end => switch (c) { | |
| 97 | '\r' => { | |
| 98 | handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings); | |
| 99 | // We only want to treat this as a newline if it's part of a CRLF pair. If it's | |
| 100 | // not, then we still want to stay in .multiline_comment_end, so that e.g. `*<\r>/` still | |
| 101 | // functions as a `*/` comment ending. Kinda crazy, but that's how the Win32 implementation works. | |
| 102 | if (formsLineEndingPair(source, '\r', index + 1)) { | |
| 103 | state = .multiline_comment; | |
| 104 | } | |
| 105 | }, | |
| 106 | '\n' => { | |
| 107 | _ = line_handler.incrementLineNumber(index); | |
| 108 | result.write(c); | |
| 109 | state = .multiline_comment; | |
| 110 | }, | |
| 111 | '/' => { | |
| 112 | state = .start; | |
| 113 | }, | |
| 114 | else => { | |
| 115 | state = .multiline_comment; | |
| 116 | }, | |
| 117 | }, | |
| 118 | .single_quoted => switch (c) { | |
| 119 | '\r', '\n' => { | |
| 120 | _ = line_handler.incrementLineNumber(index); | |
| 121 | state = .start; | |
| 122 | result.write(c); | |
| 123 | }, | |
| 124 | '\\' => { | |
| 125 | state = .single_quoted_escape; | |
| 126 | result.write(c); | |
| 127 | }, | |
| 128 | '\'' => { | |
| 129 | state = .start; | |
| 130 | result.write(c); | |
| 131 | }, | |
| 132 | else => { | |
| 133 | result.write(c); | |
| 134 | }, | |
| 135 | }, | |
| 136 | .single_quoted_escape => switch (c) { | |
| 137 | '\r', '\n' => { | |
| 138 | _ = line_handler.incrementLineNumber(index); | |
| 139 | state = .start; | |
| 140 | result.write(c); | |
| 141 | }, | |
| 142 | else => { | |
| 143 | state = .single_quoted; | |
| 144 | result.write(c); | |
| 145 | }, | |
| 146 | }, | |
| 147 | .double_quoted => switch (c) { | |
| 148 | '\r', '\n' => { | |
| 149 | _ = line_handler.incrementLineNumber(index); | |
| 150 | state = .start; | |
| 151 | result.write(c); | |
| 152 | }, | |
| 153 | '\\' => { | |
| 154 | state = .double_quoted_escape; | |
| 155 | result.write(c); | |
| 156 | }, | |
| 157 | '"' => { | |
| 158 | state = .start; | |
| 159 | result.write(c); | |
| 160 | }, | |
| 161 | else => { | |
| 162 | result.write(c); | |
| 163 | }, | |
| 164 | }, | |
| 165 | .double_quoted_escape => switch (c) { | |
| 166 | '\r', '\n' => { | |
| 167 | _ = line_handler.incrementLineNumber(index); | |
| 168 | state = .start; | |
| 169 | result.write(c); | |
| 170 | }, | |
| 171 | else => { | |
| 172 | state = .double_quoted; | |
| 173 | result.write(c); | |
| 174 | }, | |
| 175 | }, | |
| 176 | } | |
| 177 | } | |
| 178 | return result.getWritten(); | |
| 179 | } | |
| 180 | ||
| 181 | inline fn handleMultilineCarriageReturn( | |
| 182 | source: []const u8, | |
| 183 | line_handler: *LineHandler, | |
| 184 | index: usize, | |
| 185 | result: *UncheckedSliceWriter, | |
| 186 | source_mappings: ?*SourceMappings, | |
| 187 | ) void { | |
| 188 | // Note: Bare \r within a multiline comment should *not* be treated as a line ending for the | |
| 189 | // purposes of removing comments, but *should* be treated as a line ending for the | |
| 190 | // purposes of line counting/source mapping | |
| 191 | _ = line_handler.incrementLineNumber(index); | |
| 192 | // So only write the \r if it's part of a CRLF pair | |
| 193 | if (formsLineEndingPair(source, '\r', index + 1)) { | |
| 194 | result.write('\r'); | |
| 195 | } | |
| 196 | // And otherwise, we want to collapse the source mapping so that we can still know which | |
| 197 | // line came from where. | |
| 198 | else { | |
| 199 | // Because the line gets collapsed, we need to decrement line number so that | |
| 200 | // the next collapse acts on the first of the collapsed line numbers | |
| 201 | line_handler.line_number -= 1; | |
| 202 | if (source_mappings) |mappings| { | |
| 203 | mappings.collapse(line_handler.line_number, 1); | |
| 204 | } | |
| 205 | } | |
| 206 | } | |
| 207 | ||
| 208 | pub fn removeCommentsAlloc(allocator: Allocator, source: []const u8, source_mappings: ?*SourceMappings) ![]u8 { | |
| 209 | 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 | ||
| 215 | fn testRemoveComments(expected: []const u8, source: []const u8) !void { | |
| 216 | const result = try removeCommentsAlloc(std.testing.allocator, source, null); | |
| 217 | defer std.testing.allocator.free(result); | |
| 218 | ||
| 219 | try std.testing.expectEqualStrings(expected, result); | |
| 220 | } | |
| 221 | ||
| 222 | test "basic" { | |
| 223 | try testRemoveComments("", "// comment"); | |
| 224 | try testRemoveComments("", "/* comment */"); | |
| 225 | } | |
| 226 | ||
| 227 | test "mixed" { | |
| 228 | try testRemoveComments("hello", "hello// comment"); | |
| 229 | try testRemoveComments("hello", "hel/* comment */lo"); | |
| 230 | } | |
| 231 | ||
| 232 | test "within a string" { | |
| 233 | // escaped " is \" | |
| 234 | try testRemoveComments( | |
| 235 | \\blah"//som\"/*ething*/"BLAH | |
| 236 | , | |
| 237 | \\blah"//som\"/*ething*/"BLAH | |
| 238 | ); | |
| 239 | } | |
| 240 | ||
| 241 | test "line comments retain newlines" { | |
| 242 | try testRemoveComments( | |
| 243 | \\ | |
| 244 | \\ | |
| 245 | \\ | |
| 246 | , | |
| 247 | \\// comment | |
| 248 | \\// comment | |
| 249 | \\// comment | |
| 250 | ); | |
| 251 | ||
| 252 | try testRemoveComments("\r\n", "//comment\r\n"); | |
| 253 | } | |
| 254 | ||
| 255 | test "crazy" { | |
| 256 | try testRemoveComments( | |
| 257 | \\blah"/*som*/\""BLAH | |
| 258 | , | |
| 259 | \\blah"/*som*/\""/*ething*/BLAH | |
| 260 | ); | |
| 261 | ||
| 262 | try testRemoveComments( | |
| 263 | \\blah"/*som*/"BLAH RCDATA "BEGIN END | |
| 264 | \\ | |
| 265 | \\ | |
| 266 | \\hello | |
| 267 | \\" | |
| 268 | , | |
| 269 | \\blah"/*som*/"/*ething*/BLAH RCDATA "BEGIN END | |
| 270 | \\// comment | |
| 271 | \\//"blah blah" RCDATA {} | |
| 272 | \\hello | |
| 273 | \\" | |
| 274 | ); | |
| 275 | } | |
| 276 | ||
| 277 | test "multiline comment with newlines" { | |
| 278 | // bare \r is not treated as a newline | |
| 279 | try testRemoveComments("blahblah", "blah/*some\rthing*/blah"); | |
| 280 | ||
| 281 | try testRemoveComments( | |
| 282 | \\blah | |
| 283 | \\blah | |
| 284 | , | |
| 285 | \\blah/*some | |
| 286 | \\thing*/blah | |
| 287 | ); | |
| 288 | try testRemoveComments( | |
| 289 | "blah\r\nblah", | |
| 290 | "blah/*some\r\nthing*/blah", | |
| 291 | ); | |
| 292 | ||
| 293 | // handle *<not /> correctly | |
| 294 | try testRemoveComments( | |
| 295 | \\blah | |
| 296 | \\ | |
| 297 | \\ | |
| 298 | , | |
| 299 | \\blah/*some | |
| 300 | \\thing* | |
| 301 | \\/bl*ah*/ | |
| 302 | ); | |
| 303 | } | |
| 304 | ||
| 305 | test "comments appended to a line" { | |
| 306 | try testRemoveComments( | |
| 307 | \\blah | |
| 308 | \\blah | |
| 309 | , | |
| 310 | \\blah // line comment | |
| 311 | \\blah | |
| 312 | ); | |
| 313 | try testRemoveComments( | |
| 314 | "blah \r\nblah", | |
| 315 | "blah // line comment\r\nblah", | |
| 316 | ); | |
| 317 | } | |
| 318 | ||
| 319 | test "remove comments with mappings" { | |
| 320 | const allocator = std.testing.allocator; | |
| 321 | var mut_source = "blah/*\rcommented line*\r/blah".*; | |
| 322 | var mappings = SourceMappings{}; | |
| 323 | _ = try mappings.files.put(allocator, "test.rc"); | |
| 324 | try mappings.set(allocator, 1, .{ .start_line = 1, .end_line = 1, .filename_offset = 0 }); | |
| 325 | try mappings.set(allocator, 2, .{ .start_line = 2, .end_line = 2, .filename_offset = 0 }); | |
| 326 | try mappings.set(allocator, 3, .{ .start_line = 3, .end_line = 3, .filename_offset = 0 }); | |
| 327 | defer mappings.deinit(allocator); | |
| 328 | ||
| 329 | 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 | ||
| 336 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const Allocator = std.mem.Allocator; | |
| 4 | const Node = @import("ast.zig").Node; | |
| 5 | const lex = @import("lex.zig"); | |
| 6 | const Parser = @import("parse.zig").Parser; | |
| 7 | const Resource = @import("rc.zig").Resource; | |
| 8 | const Token = @import("lex.zig").Token; | |
| 9 | const literals = @import("literals.zig"); | |
| 10 | const Number = literals.Number; | |
| 11 | const SourceBytes = literals.SourceBytes; | |
| 12 | const Diagnostics = @import("errors.zig").Diagnostics; | |
| 13 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 14 | const MemoryFlags = @import("res.zig").MemoryFlags; | |
| 15 | const rc = @import("rc.zig"); | |
| 16 | const res = @import("res.zig"); | |
| 17 | const ico = @import("ico.zig"); | |
| 18 | const ani = @import("ani.zig"); | |
| 19 | const bmp = @import("bmp.zig"); | |
| 20 | const WORD = std.os.windows.WORD; | |
| 21 | const DWORD = std.os.windows.DWORD; | |
| 22 | const utils = @import("utils.zig"); | |
| 23 | const NameOrOrdinal = res.NameOrOrdinal; | |
| 24 | const CodePage = @import("code_pages.zig").CodePage; | |
| 25 | const CodePageLookup = @import("ast.zig").CodePageLookup; | |
| 26 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 27 | const windows1252 = @import("windows1252.zig"); | |
| 28 | const lang = @import("lang.zig"); | |
| 29 | const code_pages = @import("code_pages.zig"); | |
| 30 | const errors = @import("errors.zig"); | |
| 31 | const native_endian = builtin.cpu.arch.endian(); | |
| 32 | ||
| 33 | pub 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 | ||
| 61 | pub 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 | ||
| 169 | pub 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 | ||
| 2832 | pub const OpenSearchPathError = std.fs.Dir.OpenError; | |
| 2833 | ||
| 2834 | fn 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. | |
| 2847 | fn 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 | ||
| 2866 | pub 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` | |
| 2879 | pub 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 | ||
| 2905 | pub 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. | |
| 2914 | pub 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 | |
| 2939 | pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) { | |
| 2940 | return .{ .inner_writer = inner_writer, .bytes_left = bytes_left }; | |
| 2941 | } | |
| 2942 | ||
| 2943 | test "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 | ||
| 2957 | pub 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 | ||
| 3061 | pub 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 | ||
| 3092 | pub 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 | ||
| 3323 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Token = @import("lex.zig").Token; | |
| 3 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 4 | const utils = @import("utils.zig"); | |
| 5 | const rc = @import("rc.zig"); | |
| 6 | const res = @import("res.zig"); | |
| 7 | const ico = @import("ico.zig"); | |
| 8 | const bmp = @import("bmp.zig"); | |
| 9 | const parse = @import("parse.zig"); | |
| 10 | const lang = @import("lang.zig"); | |
| 11 | const CodePage = @import("code_pages.zig").CodePage; | |
| 12 | const builtin = @import("builtin"); | |
| 13 | const native_endian = builtin.cpu.arch.endian(); | |
| 14 | ||
| 15 | pub 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 | |
| 94 | pub const DiagnosticsContext = struct { | |
| 95 | diagnostics: *Diagnostics, | |
| 96 | token: Token, | |
| 97 | }; | |
| 98 | ||
| 99 | pub 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 | ||
| 825 | pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_config: std.io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void { | |
| 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 | ||
| 962 | const 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 | ||
| 1016 | fn writeSourceSlice(writer: anytype, slice: []const u8) !void { | |
| 1017 | for (slice) |c| try writeSourceByte(writer, c); | |
| 1018 | } | |
| 1019 | ||
| 1020 | inline 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 | ||
| 1036 | pub 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 | ||
| 1055 | pub 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 | ||
| 7 | const std = @import("std"); | |
| 8 | const builtin = @import("builtin"); | |
| 9 | const native_endian = builtin.cpu.arch.endian(); | |
| 10 | ||
| 11 | pub const ReadError = std.mem.Allocator.Error || error{ InvalidHeader, InvalidImageType, ImpossibleDataSize, UnexpectedEOF, ReadError }; | |
| 12 | ||
| 13 | pub 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. | |
| 41 | pub 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 | ||
| 113 | pub const ImageType = enum(u16) { | |
| 114 | icon = 1, | |
| 115 | cursor = 2, | |
| 116 | }; | |
| 117 | ||
| 118 | pub 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 | ||
| 151 | pub 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 | ||
| 198 | test "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 | ||
| 208 | test "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 | ||
| 217 | test "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 | ||
| 223 | test "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 | ||
| 229 | test "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 | ||
| 235 | pub 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). | |
| 263 | pub 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 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | /// This function is specific to how the Win32 RC command line interprets | |
| 4 | /// language IDs specified as integers. | |
| 5 | /// - Always interpreted as hexadecimal, but explicit 0x prefix is also allowed | |
| 6 | /// - Wraps on overflow of u16 | |
| 7 | /// - Stops parsing on any invalid hexadecimal digits | |
| 8 | /// - Errors if a digit is not the first char | |
| 9 | /// - `-` (negative) prefix is allowed | |
| 10 | pub fn parseInt(str: []const u8) error{InvalidLanguageId}!u16 { | |
| 11 | var result: u16 = 0; | |
| 12 | const radix: u8 = 16; | |
| 13 | var buf = str; | |
| 14 | ||
| 15 | const Prefix = enum { none, minus }; | |
| 16 | var prefix: Prefix = .none; | |
| 17 | switch (buf[0]) { | |
| 18 | '-' => { | |
| 19 | prefix = .minus; | |
| 20 | buf = buf[1..]; | |
| 21 | }, | |
| 22 | else => {}, | |
| 23 | } | |
| 24 | ||
| 25 | if (buf.len > 2 and buf[0] == '0' and buf[1] == 'x') { | |
| 26 | buf = buf[2..]; | |
| 27 | } | |
| 28 | ||
| 29 | for (buf, 0..) |c, i| { | |
| 30 | const digit = switch (c) { | |
| 31 | // On invalid digit for the radix, just stop parsing but don't fail | |
| 32 | 'a'...'f', 'A'...'F', '0'...'9' => std.fmt.charToDigit(c, radix) catch break, | |
| 33 | else => { | |
| 34 | // First digit must be valid | |
| 35 | if (i == 0) { | |
| 36 | return error.InvalidLanguageId; | |
| 37 | } | |
| 38 | break; | |
| 39 | }, | |
| 40 | }; | |
| 41 | ||
| 42 | if (result != 0) { | |
| 43 | result *%= radix; | |
| 44 | } | |
| 45 | result +%= digit; | |
| 46 | } | |
| 47 | ||
| 48 | switch (prefix) { | |
| 49 | .none => {}, | |
| 50 | .minus => result = 0 -% result, | |
| 51 | } | |
| 52 | ||
| 53 | return result; | |
| 54 | } | |
| 55 | ||
| 56 | test parseInt { | |
| 57 | try std.testing.expectEqual(@as(u16, 0x16), try parseInt("16")); | |
| 58 | try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1A")); | |
| 59 | try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1Azzzz")); | |
| 60 | try std.testing.expectEqual(@as(u16, 0xffff), try parseInt("-1")); | |
| 61 | try std.testing.expectEqual(@as(u16, 0xffea), try parseInt("-0x16")); | |
| 62 | try std.testing.expectEqual(@as(u16, 0x0), try parseInt("0o100")); | |
| 63 | try std.testing.expectEqual(@as(u16, 0x1), try parseInt("10001")); | |
| 64 | try std.testing.expectError(error.InvalidLanguageId, parseInt("--1")); | |
| 65 | try std.testing.expectError(error.InvalidLanguageId, parseInt("0xha")); | |
| 66 | try std.testing.expectError(error.InvalidLanguageId, parseInt("¹")); | |
| 67 | try std.testing.expectError(error.InvalidLanguageId, parseInt("~1")); | |
| 68 | } | |
| 69 | ||
| 70 | /// This function is specific to how the Win32 RC command line interprets | |
| 71 | /// language tags: invalid tags are rejected, but tags that don't have | |
| 72 | /// a specific assigned ID but are otherwise valid enough will get | |
| 73 | /// converted to an ID of LOCALE_CUSTOM_UNSPECIFIED. | |
| 74 | pub fn tagToInt(tag: []const u8) error{InvalidLanguageTag}!u16 { | |
| 75 | const maybe_id = try tagToId(tag); | |
| 76 | if (maybe_id) |id| { | |
| 77 | return @intFromEnum(id); | |
| 78 | } else { | |
| 79 | return LOCALE_CUSTOM_UNSPECIFIED; | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | pub fn tagToId(tag: []const u8) error{InvalidLanguageTag}!?LanguageId { | |
| 84 | const parsed = try parse(tag); | |
| 85 | // There are currently no language tags with assigned IDs that have | |
| 86 | // multiple suffixes, so we can skip the lookup. | |
| 87 | if (parsed.multiple_suffixes) return null; | |
| 88 | const longest_known_tag = comptime blk: { | |
| 89 | var len = 0; | |
| 90 | for (@typeInfo(LanguageId).Enum.fields) |field| { | |
| 91 | if (field.name.len > len) len = field.name.len; | |
| 92 | } | |
| 93 | break :blk len; | |
| 94 | }; | |
| 95 | // If the tag is longer than the longest tag that has an assigned ID, | |
| 96 | // then we can skip the lookup. | |
| 97 | if (tag.len > longest_known_tag) return null; | |
| 98 | var normalized_buf: [longest_known_tag]u8 = undefined; | |
| 99 | // To allow e.g. `de-de_phoneb` to get looked up as `de-de`, we need to | |
| 100 | // omit the suffix, but only if the tag contains a valid alternate sort order. | |
| 101 | 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 | ||
| 113 | test tagToId { | |
| 114 | try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("ar-ae")).?); | |
| 115 | try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("AR_AE")).?); | |
| 116 | try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-ng")).?); | |
| 117 | // Special case | |
| 118 | try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-Latn-NG")).?); | |
| 119 | } | |
| 120 | ||
| 121 | test "exhaustive tagToId" { | |
| 122 | inline for (@typeInfo(LanguageId).Enum.fields) |field| { | |
| 123 | const id = tagToId(field.name) catch |err| { | |
| 124 | std.debug.print("tag: {s}\n", .{field.name}); | |
| 125 | return err; | |
| 126 | }; | |
| 127 | try std.testing.expectEqual(@field(LanguageId, field.name), id orelse { | |
| 128 | std.debug.print("tag: {s}, got null\n", .{field.name}); | |
| 129 | return error.TestExpectedEqual; | |
| 130 | }); | |
| 131 | } | |
| 132 | var buf: [32]u8 = undefined; | |
| 133 | inline for (valid_alternate_sorts) |parsed_sort| { | |
| 134 | var fbs = std.io.fixedBufferStream(&buf); | |
| 135 | const writer = fbs.writer(); | |
| 136 | writer.writeAll(parsed_sort.language_code) catch unreachable; | |
| 137 | writer.writeAll("-") catch unreachable; | |
| 138 | writer.writeAll(parsed_sort.country_code.?) catch unreachable; | |
| 139 | writer.writeAll("-") catch unreachable; | |
| 140 | writer.writeAll(parsed_sort.suffix.?) catch unreachable; | |
| 141 | const expected_field_name = comptime field: { | |
| 142 | var name_buf: [5]u8 = undefined; | |
| 143 | @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 | ||
| 160 | fn normalizeTag(tag: []const u8, buf: []u8) []u8 { | |
| 161 | std.debug.assert(buf.len >= tag.len); | |
| 162 | for (tag, 0..) |c, i| { | |
| 163 | if (c == '-') | |
| 164 | buf[i] = '_' | |
| 165 | else | |
| 166 | buf[i] = std.ascii.toLower(c); | |
| 167 | } | |
| 168 | return buf[0..tag.len]; | |
| 169 | } | |
| 170 | ||
| 171 | /// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-LCID/%5bMS-LCID%5d.pdf#%5B%7B%22num%22%3A72%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C69%2C574%2C0%5D | |
| 172 | /// "When an LCID is requested for a locale without a | |
| 173 | /// permanent LCID assignment, nor a temporary | |
| 174 | /// assignment as above, the protocol will respond | |
| 175 | /// with LOCALE_CUSTOM_UNSPECIFIED for all such | |
| 176 | /// locales. Because this single value is used for | |
| 177 | /// numerous possible locale names, it is impossible to | |
| 178 | /// round trip this locale, even temporarily. | |
| 179 | /// Applications should discard this value as soon as | |
| 180 | /// possible and never persist it. If the system is | |
| 181 | /// forced to respond to a request for | |
| 182 | /// LCID_CUSTOM_UNSPECIFIED, it will fall back to | |
| 183 | /// the current user locale. This is often incorrect but | |
| 184 | /// may prevent an application or component from | |
| 185 | /// failing. As the meaning of this temporary LCID is | |
| 186 | /// unstable, it should never be used for interchange | |
| 187 | /// or persisted data. This is a 1-to-many relationship | |
| 188 | /// that is very unstable." | |
| 189 | pub const LOCALE_CUSTOM_UNSPECIFIED = 0x1000; | |
| 190 | ||
| 191 | pub const LANG_ENGLISH = 0x09; | |
| 192 | pub const SUBLANG_ENGLISH_US = 0x01; | |
| 193 | ||
| 194 | /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers | |
| 195 | pub fn MAKELANGID(primary: u10, sublang: u6) u16 { | |
| 196 | return (@as(u16, primary) << 10) | sublang; | |
| 197 | } | |
| 198 | ||
| 199 | /// Language tag format expressed as a regular expression (rough approximation): | |
| 200 | /// | |
| 201 | /// [a-zA-Z]{1,3}([-_][a-zA-Z]{4})?([-_][a-zA-Z]{2})?([-_][a-zA-Z0-9]{1,8})? | |
| 202 | /// lang | script | country | suffix | |
| 203 | /// | |
| 204 | /// Notes: | |
| 205 | /// - If lang code is 1 char, it seems to mean that everything afterwards uses suffix | |
| 206 | /// parsing rules (e.g. `a-0` and `a-00000000` are allowed). | |
| 207 | /// - There can also be any number of trailing suffix parts as long as they each | |
| 208 | /// would be a valid suffix part, e.g. `en-us-blah-blah1-blah2-blah3` is allowed. | |
| 209 | /// - When doing lookups, trailing suffix parts are taken into account, e.g. | |
| 210 | /// `ca-es-valencia` is not considered equivalent to `ca-es-valencia-blah`. | |
| 211 | /// - A suffix is only allowed if: | |
| 212 | /// + Lang code is 1 char long, or | |
| 213 | /// + A country code is present, or | |
| 214 | /// + A script tag is not present and: | |
| 215 | /// - the suffix is numeric-only and has a length of 3, or | |
| 216 | /// - the lang is `qps` and the suffix is `ploca` or `plocm` | |
| 217 | pub fn parse(lang_tag: []const u8) error{InvalidLanguageTag}!Parsed { | |
| 218 | var it = std.mem.splitAny(u8, lang_tag, "-_"); | |
| 219 | const lang_code = it.first(); | |
| 220 | const is_valid_lang_code = lang_code.len >= 1 and lang_code.len <= 3 and isAllAlphabetic(lang_code); | |
| 221 | if (!is_valid_lang_code) return error.InvalidLanguageTag; | |
| 222 | var parsed = Parsed{ | |
| 223 | .language_code = lang_code, | |
| 224 | }; | |
| 225 | // The second part could be a script tag, a country code, or a suffix | |
| 226 | if (it.next()) |part_str| { | |
| 227 | // The lang code being length 1 behaves strangely, so fully special case it. | |
| 228 | if (lang_code.len == 1) { | |
| 229 | // This is almost certainly not the 'right' way to do this, but I don't have a method | |
| 230 | // to determine how exactly these language tags are parsed, and it seems like | |
| 231 | // suffix parsing rules apply generally (digits allowed, length of 1 to 8). | |
| 232 | // | |
| 233 | // However, because we want to be able to lookup `x-iv-mathan` normally without | |
| 234 | // `multiple_suffixes` being set to true, we need to make sure to treat two-length | |
| 235 | // alphabetic parts as a country code. | |
| 236 | if (part_str.len == 2 and isAllAlphabetic(part_str)) { | |
| 237 | parsed.country_code = part_str; | |
| 238 | } | |
| 239 | // Everything else, though, we can just throw into the suffix as long as the normal | |
| 240 | // rules apply. | |
| 241 | else if (part_str.len > 0 and part_str.len <= 8 and isAllAlphanumeric(part_str)) { | |
| 242 | parsed.suffix = part_str; | |
| 243 | } else { | |
| 244 | return error.InvalidLanguageTag; | |
| 245 | } | |
| 246 | } else if (part_str.len == 4 and isAllAlphabetic(part_str)) { | |
| 247 | parsed.script_tag = part_str; | |
| 248 | } else if (part_str.len == 2 and isAllAlphabetic(part_str)) { | |
| 249 | parsed.country_code = part_str; | |
| 250 | } | |
| 251 | // Only a 3-len numeric suffix is allowed as the second part of a tag | |
| 252 | else if (part_str.len == 3 and isAllNumeric(part_str)) { | |
| 253 | parsed.suffix = part_str; | |
| 254 | } | |
| 255 | // Special case for qps-ploca and qps-plocm | |
| 256 | else if (std.ascii.eqlIgnoreCase(lang_code, "qps") and | |
| 257 | (std.ascii.eqlIgnoreCase(part_str, "ploca") or | |
| 258 | std.ascii.eqlIgnoreCase(part_str, "plocm"))) | |
| 259 | { | |
| 260 | parsed.suffix = part_str; | |
| 261 | } else { | |
| 262 | return error.InvalidLanguageTag; | |
| 263 | } | |
| 264 | } else { | |
| 265 | // If there's no part besides a 1-len lang code, then it is malformed | |
| 266 | if (lang_code.len == 1) return error.InvalidLanguageTag; | |
| 267 | return parsed; | |
| 268 | } | |
| 269 | if (parsed.script_tag != null) { | |
| 270 | if (it.next()) |part_str| { | |
| 271 | if (part_str.len == 2 and isAllAlphabetic(part_str)) { | |
| 272 | parsed.country_code = part_str; | |
| 273 | } else { | |
| 274 | // Suffix is not allowed when a country code is not present. | |
| 275 | return error.InvalidLanguageTag; | |
| 276 | } | |
| 277 | } else { | |
| 278 | return parsed; | |
| 279 | } | |
| 280 | } | |
| 281 | // We've now parsed any potential script tag/country codes, so anything remaining | |
| 282 | // is a suffix | |
| 283 | while (it.next()) |part_str| { | |
| 284 | if (part_str.len == 0 or part_str.len > 8 or !isAllAlphanumeric(part_str)) { | |
| 285 | return error.InvalidLanguageTag; | |
| 286 | } | |
| 287 | if (parsed.suffix == null) { | |
| 288 | parsed.suffix = part_str; | |
| 289 | } else { | |
| 290 | // In theory we could return early here but we still want to validate | |
| 291 | // that each part is a valid suffix all the way to the end, e.g. | |
| 292 | // we should reject `en-us-suffix-a-b-c-!!!` because of the invalid `!!!` | |
| 293 | // suffix part. | |
| 294 | parsed.multiple_suffixes = true; | |
| 295 | } | |
| 296 | } | |
| 297 | return parsed; | |
| 298 | } | |
| 299 | ||
| 300 | pub const Parsed = struct { | |
| 301 | language_code: []const u8, | |
| 302 | script_tag: ?[]const u8 = null, | |
| 303 | country_code: ?[]const u8 = null, | |
| 304 | /// Can be a sort order (e.g. phoneb) or something like valencia, 001, etc | |
| 305 | suffix: ?[]const u8 = null, | |
| 306 | /// There can be any number of suffixes, but we don't need to care what their | |
| 307 | /// values are, we just need to know if any exist so that e.g. `ca-es-valencia-blah` | |
| 308 | /// can be seen as different from `ca-es-valencia`. Storing this as a bool | |
| 309 | /// allows us to avoid needing either (a) dynamic allocation or (b) a limit to | |
| 310 | /// the number of suffixes allowed when parsing. | |
| 311 | multiple_suffixes: bool = false, | |
| 312 | ||
| 313 | pub fn isSuffixValidSortOrder(self: Parsed) bool { | |
| 314 | if (self.country_code == null) return false; | |
| 315 | if (self.suffix == null) return false; | |
| 316 | if (self.script_tag != null) return false; | |
| 317 | if (self.multiple_suffixes) return false; | |
| 318 | for (valid_alternate_sorts) |valid_sort| { | |
| 319 | if (std.ascii.eqlIgnoreCase(valid_sort.language_code, self.language_code) and | |
| 320 | std.ascii.eqlIgnoreCase(valid_sort.country_code.?, self.country_code.?) and | |
| 321 | std.ascii.eqlIgnoreCase(valid_sort.suffix.?, self.suffix.?)) | |
| 322 | { | |
| 323 | return true; | |
| 324 | } | |
| 325 | } | |
| 326 | return false; | |
| 327 | } | |
| 328 | }; | |
| 329 | ||
| 330 | /// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f | |
| 331 | /// See the table following this text: "Alternate sorts can be selected by using one of the identifiers from the following table." | |
| 332 | const valid_alternate_sorts = [_]Parsed{ | |
| 333 | // Note: x-IV-mathan is omitted due to how lookups are implemented. | |
| 334 | // This table is used to make e.g. `de-de_phoneb` get looked up | |
| 335 | // as `de-de` (the suffix is omitted for the lookup), but x-iv-mathan | |
| 336 | // instead needs to be looked up with the suffix included because | |
| 337 | // `x-iv` is not a tag with an assigned ID. | |
| 338 | .{ .language_code = "de", .country_code = "de", .suffix = "phoneb" }, | |
| 339 | .{ .language_code = "hu", .country_code = "hu", .suffix = "tchncl" }, | |
| 340 | .{ .language_code = "ka", .country_code = "ge", .suffix = "modern" }, | |
| 341 | .{ .language_code = "zh", .country_code = "cn", .suffix = "stroke" }, | |
| 342 | .{ .language_code = "zh", .country_code = "sg", .suffix = "stroke" }, | |
| 343 | .{ .language_code = "zh", .country_code = "mo", .suffix = "stroke" }, | |
| 344 | .{ .language_code = "zh", .country_code = "tw", .suffix = "pronun" }, | |
| 345 | .{ .language_code = "zh", .country_code = "tw", .suffix = "radstr" }, | |
| 346 | .{ .language_code = "ja", .country_code = "jp", .suffix = "radstr" }, | |
| 347 | .{ .language_code = "zh", .country_code = "hk", .suffix = "radstr" }, | |
| 348 | .{ .language_code = "zh", .country_code = "mo", .suffix = "radstr" }, | |
| 349 | .{ .language_code = "zh", .country_code = "cn", .suffix = "phoneb" }, | |
| 350 | .{ .language_code = "zh", .country_code = "sg", .suffix = "phoneb" }, | |
| 351 | }; | |
| 352 | ||
| 353 | test "parse" { | |
| 354 | try std.testing.expectEqualDeep(Parsed{ | |
| 355 | .language_code = "en", | |
| 356 | }, try parse("en")); | |
| 357 | try std.testing.expectEqualDeep(Parsed{ | |
| 358 | .language_code = "en", | |
| 359 | .country_code = "us", | |
| 360 | }, try parse("en-us")); | |
| 361 | try std.testing.expectEqualDeep(Parsed{ | |
| 362 | .language_code = "en", | |
| 363 | .suffix = "123", | |
| 364 | }, try parse("en-123")); | |
| 365 | try std.testing.expectEqualDeep(Parsed{ | |
| 366 | .language_code = "en", | |
| 367 | .suffix = "123", | |
| 368 | .multiple_suffixes = true, | |
| 369 | }, try parse("en-123-blah")); | |
| 370 | try std.testing.expectEqualDeep(Parsed{ | |
| 371 | .language_code = "en", | |
| 372 | .country_code = "us", | |
| 373 | .suffix = "123", | |
| 374 | .multiple_suffixes = true, | |
| 375 | }, try parse("en-us_123-blah")); | |
| 376 | try std.testing.expectEqualDeep(Parsed{ | |
| 377 | .language_code = "eng", | |
| 378 | .script_tag = "Latn", | |
| 379 | }, try parse("eng-Latn")); | |
| 380 | try std.testing.expectEqualDeep(Parsed{ | |
| 381 | .language_code = "eng", | |
| 382 | .script_tag = "Latn", | |
| 383 | }, try parse("eng-Latn")); | |
| 384 | try std.testing.expectEqualDeep(Parsed{ | |
| 385 | .language_code = "ff", | |
| 386 | .script_tag = "Latn", | |
| 387 | .country_code = "NG", | |
| 388 | }, try parse("ff-Latn-NG")); | |
| 389 | try std.testing.expectEqualDeep(Parsed{ | |
| 390 | .language_code = "qps", | |
| 391 | .suffix = "Plocm", | |
| 392 | }, try parse("qps-Plocm")); | |
| 393 | try std.testing.expectEqualDeep(Parsed{ | |
| 394 | .language_code = "qps", | |
| 395 | .suffix = "ploca", | |
| 396 | }, try parse("qps-ploca")); | |
| 397 | try std.testing.expectEqualDeep(Parsed{ | |
| 398 | .language_code = "x", | |
| 399 | .country_code = "IV", | |
| 400 | .suffix = "mathan", | |
| 401 | }, try parse("x-IV-mathan")); | |
| 402 | try std.testing.expectEqualDeep(Parsed{ | |
| 403 | .language_code = "a", | |
| 404 | .suffix = "a", | |
| 405 | }, try parse("a-a")); | |
| 406 | try std.testing.expectEqualDeep(Parsed{ | |
| 407 | .language_code = "a", | |
| 408 | .suffix = "000", | |
| 409 | }, try parse("a-000")); | |
| 410 | try std.testing.expectEqualDeep(Parsed{ | |
| 411 | .language_code = "a", | |
| 412 | .suffix = "00000000", | |
| 413 | }, try parse("a-00000000")); | |
| 414 | // suffix not allowed if script tag is present without country code | |
| 415 | try std.testing.expectError(error.InvalidLanguageTag, parse("eng-Latn-suffix")); | |
| 416 | // suffix must be 3 numeric digits if neither script tag nor country code is present | |
| 417 | try std.testing.expectError(error.InvalidLanguageTag, parse("eng-suffix")); | |
| 418 | try std.testing.expectError(error.InvalidLanguageTag, parse("en-plocm")); | |
| 419 | // 1-len lang code is not allowed if it's the only part | |
| 420 | try std.testing.expectError(error.InvalidLanguageTag, parse("e")); | |
| 421 | } | |
| 422 | ||
| 423 | fn isAllAlphabetic(str: []const u8) bool { | |
| 424 | for (str) |c| { | |
| 425 | if (!std.ascii.isAlphabetic(c)) return false; | |
| 426 | } | |
| 427 | return true; | |
| 428 | } | |
| 429 | ||
| 430 | fn isAllAlphanumeric(str: []const u8) bool { | |
| 431 | for (str) |c| { | |
| 432 | if (!std.ascii.isAlphanumeric(c)) return false; | |
| 433 | } | |
| 434 | return true; | |
| 435 | } | |
| 436 | ||
| 437 | fn isAllNumeric(str: []const u8) bool { | |
| 438 | for (str) |c| { | |
| 439 | if (!std.ascii.isDigit(c)) return false; | |
| 440 | } | |
| 441 | return true; | |
| 442 | } | |
| 443 | ||
| 444 | /// Derived from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f | |
| 445 | /// - Protocol Revision: 15.0 | |
| 446 | /// - Language / Language ID / Language Tag table in Appendix A | |
| 447 | /// - Removed all rows that have Language ID 0x1000 (LOCALE_CUSTOM_UNSPECIFIED) | |
| 448 | /// - Normalized each language tag (lowercased, replaced all `-` with `_`) | |
| 449 | /// - There is one special case where two tags are mapped to the same ID, the following | |
| 450 | /// has been omitted and must be special cased during lookup to map to the ID ff_ng / 0x0467. | |
| 451 | /// ff_latn_ng = 0x0467, // Fulah (Latin), Nigeria | |
| 452 | /// - x_iv_mathan has been added which is not in the table but does appear in the Alternate sorts | |
| 453 | /// table as 0x007F (LANG_INVARIANT). | |
| 454 | pub const LanguageId = enum(u16) { | |
| 455 | // Language tag = Language ID, // Language, Location (or type) | |
| 456 | af = 0x0036, // Afrikaans | |
| 457 | af_za = 0x0436, // Afrikaans, South Africa | |
| 458 | sq = 0x001C, // Albanian | |
| 459 | sq_al = 0x041C, // Albanian, Albania | |
| 460 | gsw = 0x0084, // Alsatian | |
| 461 | gsw_fr = 0x0484, // Alsatian, France | |
| 462 | am = 0x005E, // Amharic | |
| 463 | am_et = 0x045E, // Amharic, Ethiopia | |
| 464 | ar = 0x0001, // Arabic | |
| 465 | ar_dz = 0x1401, // Arabic, Algeria | |
| 466 | ar_bh = 0x3C01, // Arabic, Bahrain | |
| 467 | ar_eg = 0x0c01, // Arabic, Egypt | |
| 468 | ar_iq = 0x0801, // Arabic, Iraq | |
| 469 | ar_jo = 0x2C01, // Arabic, Jordan | |
| 470 | ar_kw = 0x3401, // Arabic, Kuwait | |
| 471 | ar_lb = 0x3001, // Arabic, Lebanon | |
| 472 | ar_ly = 0x1001, // Arabic, Libya | |
| 473 | ar_ma = 0x1801, // Arabic, Morocco | |
| 474 | ar_om = 0x2001, // Arabic, Oman | |
| 475 | ar_qa = 0x4001, // Arabic, Qatar | |
| 476 | ar_sa = 0x0401, // Arabic, Saudi Arabia | |
| 477 | ar_sy = 0x2801, // Arabic, Syria | |
| 478 | ar_tn = 0x1C01, // Arabic, Tunisia | |
| 479 | ar_ae = 0x3801, // Arabic, U.A.E. | |
| 480 | ar_ye = 0x2401, // Arabic, Yemen | |
| 481 | hy = 0x002B, // Armenian | |
| 482 | hy_am = 0x042B, // Armenian, Armenia | |
| 483 | as = 0x004D, // Assamese | |
| 484 | as_in = 0x044D, // Assamese, India | |
| 485 | az_cyrl = 0x742C, // Azerbaijani (Cyrillic) | |
| 486 | az_cyrl_az = 0x082C, // Azerbaijani (Cyrillic), Azerbaijan | |
| 487 | az = 0x002C, // Azerbaijani (Latin) | |
| 488 | az_latn = 0x782C, // Azerbaijani (Latin) | |
| 489 | az_latn_az = 0x042C, // Azerbaijani (Latin), Azerbaijan | |
| 490 | bn = 0x0045, // Bangla | |
| 491 | bn_bd = 0x0845, // Bangla, Bangladesh | |
| 492 | bn_in = 0x0445, // Bangla, India | |
| 493 | ba = 0x006D, // Bashkir | |
| 494 | ba_ru = 0x046D, // Bashkir, Russia | |
| 495 | eu = 0x002D, // Basque | |
| 496 | eu_es = 0x042D, // Basque, Spain | |
| 497 | be = 0x0023, // Belarusian | |
| 498 | be_by = 0x0423, // Belarusian, Belarus | |
| 499 | bs_cyrl = 0x641A, // Bosnian (Cyrillic) | |
| 500 | bs_cyrl_ba = 0x201A, // Bosnian (Cyrillic), Bosnia and Herzegovina | |
| 501 | bs_latn = 0x681A, // Bosnian (Latin) | |
| 502 | bs = 0x781A, // Bosnian (Latin) | |
| 503 | bs_latn_ba = 0x141A, // Bosnian (Latin), Bosnia and Herzegovina | |
| 504 | br = 0x007E, // Breton | |
| 505 | br_fr = 0x047E, // Breton, France | |
| 506 | bg = 0x0002, // Bulgarian | |
| 507 | bg_bg = 0x0402, // Bulgarian, Bulgaria | |
| 508 | my = 0x0055, // Burmese | |
| 509 | my_mm = 0x0455, // Burmese, Myanmar | |
| 510 | ca = 0x0003, // Catalan | |
| 511 | ca_es = 0x0403, // Catalan, Spain | |
| 512 | tzm_arab_ma = 0x045F, // Central Atlas Tamazight (Arabic), Morocco | |
| 513 | ku = 0x0092, // Central Kurdish | |
| 514 | ku_arab = 0x7c92, // Central Kurdish | |
| 515 | ku_arab_iq = 0x0492, // Central Kurdish, Iraq | |
| 516 | chr = 0x005C, // Cherokee | |
| 517 | chr_cher = 0x7c5C, // Cherokee | |
| 518 | chr_cher_us = 0x045C, // Cherokee, United States | |
| 519 | zh_hans = 0x0004, // Chinese (Simplified) | |
| 520 | zh = 0x7804, // Chinese (Simplified) | |
| 521 | zh_cn = 0x0804, // Chinese (Simplified), People's Republic of China | |
| 522 | zh_sg = 0x1004, // Chinese (Simplified), Singapore | |
| 523 | zh_hant = 0x7C04, // Chinese (Traditional) | |
| 524 | zh_hk = 0x0C04, // Chinese (Traditional), Hong Kong S.A.R. | |
| 525 | zh_mo = 0x1404, // Chinese (Traditional), Macao S.A.R. | |
| 526 | zh_tw = 0x0404, // Chinese (Traditional), Taiwan | |
| 527 | co = 0x0083, // Corsican | |
| 528 | co_fr = 0x0483, // Corsican, France | |
| 529 | hr = 0x001A, // Croatian | |
| 530 | hr_hr = 0x041A, // Croatian, Croatia | |
| 531 | hr_ba = 0x101A, // Croatian (Latin), Bosnia and Herzegovina | |
| 532 | cs = 0x0005, // Czech | |
| 533 | cs_cz = 0x0405, // Czech, Czech Republic | |
| 534 | da = 0x0006, // Danish | |
| 535 | da_dk = 0x0406, // Danish, Denmark | |
| 536 | prs = 0x008C, // Dari | |
| 537 | prs_af = 0x048C, // Dari, Afghanistan | |
| 538 | dv = 0x0065, // Divehi | |
| 539 | dv_mv = 0x0465, // Divehi, Maldives | |
| 540 | nl = 0x0013, // Dutch | |
| 541 | nl_be = 0x0813, // Dutch, Belgium | |
| 542 | nl_nl = 0x0413, // Dutch, Netherlands | |
| 543 | dz_bt = 0x0C51, // Dzongkha, Bhutan | |
| 544 | en = 0x0009, // English | |
| 545 | en_au = 0x0C09, // English, Australia | |
| 546 | en_bz = 0x2809, // English, Belize | |
| 547 | en_ca = 0x1009, // English, Canada | |
| 548 | en_029 = 0x2409, // English, Caribbean | |
| 549 | en_hk = 0x3C09, // English, Hong Kong | |
| 550 | en_in = 0x4009, // English, India | |
| 551 | en_ie = 0x1809, // English, Ireland | |
| 552 | en_jm = 0x2009, // English, Jamaica | |
| 553 | en_my = 0x4409, // English, Malaysia | |
| 554 | en_nz = 0x1409, // English, New Zealand | |
| 555 | en_ph = 0x3409, // English, Republic of the Philippines | |
| 556 | en_sg = 0x4809, // English, Singapore | |
| 557 | en_za = 0x1C09, // English, South Africa | |
| 558 | en_tt = 0x2c09, // English, Trinidad and Tobago | |
| 559 | en_ae = 0x4C09, // English, United Arab Emirates | |
| 560 | en_gb = 0x0809, // English, United Kingdom | |
| 561 | en_us = 0x0409, // English, United States | |
| 562 | en_zw = 0x3009, // English, Zimbabwe | |
| 563 | et = 0x0025, // Estonian | |
| 564 | et_ee = 0x0425, // Estonian, Estonia | |
| 565 | fo = 0x0038, // Faroese | |
| 566 | fo_fo = 0x0438, // Faroese, Faroe Islands | |
| 567 | fil = 0x0064, // Filipino | |
| 568 | fil_ph = 0x0464, // Filipino, Philippines | |
| 569 | fi = 0x000B, // Finnish | |
| 570 | fi_fi = 0x040B, // Finnish, Finland | |
| 571 | fr = 0x000C, // French | |
| 572 | fr_be = 0x080C, // French, Belgium | |
| 573 | fr_cm = 0x2c0C, // French, Cameroon | |
| 574 | fr_ca = 0x0c0C, // French, Canada | |
| 575 | fr_029 = 0x1C0C, // French, Caribbean | |
| 576 | fr_cd = 0x240C, // French, Congo, DRC | |
| 577 | fr_ci = 0x300C, // French, Côte d'Ivoire | |
| 578 | fr_fr = 0x040C, // French, France | |
| 579 | fr_ht = 0x3c0C, // French, Haiti | |
| 580 | fr_lu = 0x140C, // French, Luxembourg | |
| 581 | fr_ml = 0x340C, // French, Mali | |
| 582 | fr_ma = 0x380C, // French, Morocco | |
| 583 | fr_mc = 0x180C, // French, Principality of Monaco | |
| 584 | fr_re = 0x200C, // French, Reunion | |
| 585 | fr_sn = 0x280C, // French, Senegal | |
| 586 | fr_ch = 0x100C, // French, Switzerland | |
| 587 | fy = 0x0062, // Frisian | |
| 588 | fy_nl = 0x0462, // Frisian, Netherlands | |
| 589 | ff = 0x0067, // Fulah | |
| 590 | ff_latn = 0x7C67, // Fulah (Latin) | |
| 591 | ff_ng = 0x0467, // Fulah, Nigeria | |
| 592 | ff_latn_sn = 0x0867, // Fulah, Senegal | |
| 593 | gl = 0x0056, // Galician | |
| 594 | gl_es = 0x0456, // Galician, Spain | |
| 595 | ka = 0x0037, // Georgian | |
| 596 | ka_ge = 0x0437, // Georgian, Georgia | |
| 597 | de = 0x0007, // German | |
| 598 | de_at = 0x0C07, // German, Austria | |
| 599 | de_de = 0x0407, // German, Germany | |
| 600 | de_li = 0x1407, // German, Liechtenstein | |
| 601 | de_lu = 0x1007, // German, Luxembourg | |
| 602 | de_ch = 0x0807, // German, Switzerland | |
| 603 | el = 0x0008, // Greek | |
| 604 | el_gr = 0x0408, // Greek, Greece | |
| 605 | kl = 0x006F, // Greenlandic | |
| 606 | kl_gl = 0x046F, // Greenlandic, Greenland | |
| 607 | gn = 0x0074, // Guarani | |
| 608 | gn_py = 0x0474, // Guarani, Paraguay | |
| 609 | gu = 0x0047, // Gujarati | |
| 610 | gu_in = 0x0447, // Gujarati, India | |
| 611 | ha = 0x0068, // Hausa (Latin) | |
| 612 | ha_latn = 0x7C68, // Hausa (Latin) | |
| 613 | ha_latn_ng = 0x0468, // Hausa (Latin), Nigeria | |
| 614 | haw = 0x0075, // Hawaiian | |
| 615 | haw_us = 0x0475, // Hawaiian, United States | |
| 616 | he = 0x000D, // Hebrew | |
| 617 | he_il = 0x040D, // Hebrew, Israel | |
| 618 | hi = 0x0039, // Hindi | |
| 619 | hi_in = 0x0439, // Hindi, India | |
| 620 | hu = 0x000E, // Hungarian | |
| 621 | hu_hu = 0x040E, // Hungarian, Hungary | |
| 622 | is = 0x000F, // Icelandic | |
| 623 | is_is = 0x040F, // Icelandic, Iceland | |
| 624 | ig = 0x0070, // Igbo | |
| 625 | ig_ng = 0x0470, // Igbo, Nigeria | |
| 626 | id = 0x0021, // Indonesian | |
| 627 | id_id = 0x0421, // Indonesian, Indonesia | |
| 628 | iu = 0x005D, // Inuktitut (Latin) | |
| 629 | iu_latn = 0x7C5D, // Inuktitut (Latin) | |
| 630 | iu_latn_ca = 0x085D, // Inuktitut (Latin), Canada | |
| 631 | iu_cans = 0x785D, // Inuktitut (Syllabics) | |
| 632 | iu_cans_ca = 0x045d, // Inuktitut (Syllabics), Canada | |
| 633 | ga = 0x003C, // Irish | |
| 634 | ga_ie = 0x083C, // Irish, Ireland | |
| 635 | it = 0x0010, // Italian | |
| 636 | it_it = 0x0410, // Italian, Italy | |
| 637 | it_ch = 0x0810, // Italian, Switzerland | |
| 638 | ja = 0x0011, // Japanese | |
| 639 | ja_jp = 0x0411, // Japanese, Japan | |
| 640 | kn = 0x004B, // Kannada | |
| 641 | kn_in = 0x044B, // Kannada, India | |
| 642 | kr_latn_ng = 0x0471, // Kanuri (Latin), Nigeria | |
| 643 | ks = 0x0060, // Kashmiri | |
| 644 | ks_arab = 0x0460, // Kashmiri, Perso-Arabic | |
| 645 | ks_deva_in = 0x0860, // Kashmiri (Devanagari), India | |
| 646 | kk = 0x003F, // Kazakh | |
| 647 | kk_kz = 0x043F, // Kazakh, Kazakhstan | |
| 648 | km = 0x0053, // Khmer | |
| 649 | km_kh = 0x0453, // Khmer, Cambodia | |
| 650 | quc = 0x0086, // K'iche | |
| 651 | quc_latn_gt = 0x0486, // K'iche, Guatemala | |
| 652 | rw = 0x0087, // Kinyarwanda | |
| 653 | rw_rw = 0x0487, // Kinyarwanda, Rwanda | |
| 654 | sw = 0x0041, // Kiswahili | |
| 655 | sw_ke = 0x0441, // Kiswahili, Kenya | |
| 656 | kok = 0x0057, // Konkani | |
| 657 | kok_in = 0x0457, // Konkani, India | |
| 658 | ko = 0x0012, // Korean | |
| 659 | ko_kr = 0x0412, // Korean, Korea | |
| 660 | ky = 0x0040, // Kyrgyz | |
| 661 | ky_kg = 0x0440, // Kyrgyz, Kyrgyzstan | |
| 662 | lo = 0x0054, // Lao | |
| 663 | lo_la = 0x0454, // Lao, Lao P.D.R. | |
| 664 | la_va = 0x0476, // Latin, Vatican City | |
| 665 | lv = 0x0026, // Latvian | |
| 666 | lv_lv = 0x0426, // Latvian, Latvia | |
| 667 | lt = 0x0027, // Lithuanian | |
| 668 | lt_lt = 0x0427, // Lithuanian, Lithuania | |
| 669 | dsb = 0x7C2E, // Lower Sorbian | |
| 670 | dsb_de = 0x082E, // Lower Sorbian, Germany | |
| 671 | lb = 0x006E, // Luxembourgish | |
| 672 | lb_lu = 0x046E, // Luxembourgish, Luxembourg | |
| 673 | mk = 0x002F, // Macedonian | |
| 674 | mk_mk = 0x042F, // Macedonian, North Macedonia | |
| 675 | ms = 0x003E, // Malay | |
| 676 | ms_bn = 0x083E, // Malay, Brunei Darussalam | |
| 677 | ms_my = 0x043E, // Malay, Malaysia | |
| 678 | ml = 0x004C, // Malayalam | |
| 679 | ml_in = 0x044C, // Malayalam, India | |
| 680 | mt = 0x003A, // Maltese | |
| 681 | mt_mt = 0x043A, // Maltese, Malta | |
| 682 | mi = 0x0081, // Maori | |
| 683 | mi_nz = 0x0481, // Maori, New Zealand | |
| 684 | arn = 0x007A, // Mapudungun | |
| 685 | arn_cl = 0x047A, // Mapudungun, Chile | |
| 686 | mr = 0x004E, // Marathi | |
| 687 | mr_in = 0x044E, // Marathi, India | |
| 688 | moh = 0x007C, // Mohawk | |
| 689 | moh_ca = 0x047C, // Mohawk, Canada | |
| 690 | mn = 0x0050, // Mongolian (Cyrillic) | |
| 691 | mn_cyrl = 0x7850, // Mongolian (Cyrillic) | |
| 692 | mn_mn = 0x0450, // Mongolian (Cyrillic), Mongolia | |
| 693 | mn_mong = 0x7C50, // Mongolian (Traditional Mongolian) | |
| 694 | mn_mong_cn = 0x0850, // Mongolian (Traditional Mongolian), People's Republic of China | |
| 695 | mn_mong_mn = 0x0C50, // Mongolian (Traditional Mongolian), Mongolia | |
| 696 | ne = 0x0061, // Nepali | |
| 697 | ne_in = 0x0861, // Nepali, India | |
| 698 | ne_np = 0x0461, // Nepali, Nepal | |
| 699 | no = 0x0014, // Norwegian (Bokmal) | |
| 700 | nb = 0x7C14, // Norwegian (Bokmal) | |
| 701 | nb_no = 0x0414, // Norwegian (Bokmal), Norway | |
| 702 | nn = 0x7814, // Norwegian (Nynorsk) | |
| 703 | nn_no = 0x0814, // Norwegian (Nynorsk), Norway | |
| 704 | oc = 0x0082, // Occitan | |
| 705 | oc_fr = 0x0482, // Occitan, France | |
| 706 | @"or" = 0x0048, // Odia | |
| 707 | or_in = 0x0448, // Odia, India | |
| 708 | om = 0x0072, // Oromo | |
| 709 | om_et = 0x0472, // Oromo, Ethiopia | |
| 710 | ps = 0x0063, // Pashto | |
| 711 | ps_af = 0x0463, // Pashto, Afghanistan | |
| 712 | fa = 0x0029, // Persian | |
| 713 | fa_ir = 0x0429, // Persian, Iran | |
| 714 | pl = 0x0015, // Polish | |
| 715 | pl_pl = 0x0415, // Polish, Poland | |
| 716 | pt = 0x0016, // Portuguese | |
| 717 | pt_br = 0x0416, // Portuguese, Brazil | |
| 718 | pt_pt = 0x0816, // Portuguese, Portugal | |
| 719 | qps_ploca = 0x05FE, // Pseudo Language, Pseudo locale for east Asian/complex script localization testing | |
| 720 | qps_ploc = 0x0501, // Pseudo Language, Pseudo locale used for localization testing | |
| 721 | qps_plocm = 0x09FF, // Pseudo Language, Pseudo locale used for localization testing of mirrored locales | |
| 722 | pa = 0x0046, // Punjabi | |
| 723 | pa_arab = 0x7C46, // Punjabi | |
| 724 | pa_in = 0x0446, // Punjabi, India | |
| 725 | pa_arab_pk = 0x0846, // Punjabi, Islamic Republic of Pakistan | |
| 726 | quz = 0x006B, // Quechua | |
| 727 | quz_bo = 0x046B, // Quechua, Bolivia | |
| 728 | quz_ec = 0x086B, // Quechua, Ecuador | |
| 729 | quz_pe = 0x0C6B, // Quechua, Peru | |
| 730 | ro = 0x0018, // Romanian | |
| 731 | ro_md = 0x0818, // Romanian, Moldova | |
| 732 | ro_ro = 0x0418, // Romanian, Romania | |
| 733 | rm = 0x0017, // Romansh | |
| 734 | rm_ch = 0x0417, // Romansh, Switzerland | |
| 735 | ru = 0x0019, // Russian | |
| 736 | ru_md = 0x0819, // Russian, Moldova | |
| 737 | ru_ru = 0x0419, // Russian, Russia | |
| 738 | sah = 0x0085, // Sakha | |
| 739 | sah_ru = 0x0485, // Sakha, Russia | |
| 740 | smn = 0x703B, // Sami (Inari) | |
| 741 | smn_fi = 0x243B, // Sami (Inari), Finland | |
| 742 | smj = 0x7C3B, // Sami (Lule) | |
| 743 | smj_no = 0x103B, // Sami (Lule), Norway | |
| 744 | smj_se = 0x143B, // Sami (Lule), Sweden | |
| 745 | se = 0x003B, // Sami (Northern) | |
| 746 | se_fi = 0x0C3B, // Sami (Northern), Finland | |
| 747 | se_no = 0x043B, // Sami (Northern), Norway | |
| 748 | se_se = 0x083B, // Sami (Northern), Sweden | |
| 749 | sms = 0x743B, // Sami (Skolt) | |
| 750 | sms_fi = 0x203B, // Sami (Skolt), Finland | |
| 751 | sma = 0x783B, // Sami (Southern) | |
| 752 | sma_no = 0x183B, // Sami (Southern), Norway | |
| 753 | sma_se = 0x1C3B, // Sami (Southern), Sweden | |
| 754 | sa = 0x004F, // Sanskrit | |
| 755 | sa_in = 0x044F, // Sanskrit, India | |
| 756 | gd = 0x0091, // Scottish Gaelic | |
| 757 | gd_gb = 0x0491, // Scottish Gaelic, United Kingdom | |
| 758 | sr_cyrl = 0x6C1A, // Serbian (Cyrillic) | |
| 759 | sr_cyrl_ba = 0x1C1A, // Serbian (Cyrillic), Bosnia and Herzegovina | |
| 760 | sr_cyrl_me = 0x301A, // Serbian (Cyrillic), Montenegro | |
| 761 | sr_cyrl_rs = 0x281A, // Serbian (Cyrillic), Serbia | |
| 762 | sr_cyrl_cs = 0x0C1A, // Serbian (Cyrillic), Serbia and Montenegro (Former) | |
| 763 | sr_latn = 0x701A, // Serbian (Latin) | |
| 764 | sr = 0x7C1A, // Serbian (Latin) | |
| 765 | sr_latn_ba = 0x181A, // Serbian (Latin), Bosnia and Herzegovina | |
| 766 | sr_latn_me = 0x2c1A, // Serbian (Latin), Montenegro | |
| 767 | sr_latn_rs = 0x241A, // Serbian (Latin), Serbia | |
| 768 | sr_latn_cs = 0x081A, // Serbian (Latin), Serbia and Montenegro (Former) | |
| 769 | nso = 0x006C, // Sesotho sa Leboa | |
| 770 | nso_za = 0x046C, // Sesotho sa Leboa, South Africa | |
| 771 | tn = 0x0032, // Setswana | |
| 772 | tn_bw = 0x0832, // Setswana, Botswana | |
| 773 | tn_za = 0x0432, // Setswana, South Africa | |
| 774 | sd = 0x0059, // Sindhi | |
| 775 | sd_arab = 0x7C59, // Sindhi | |
| 776 | sd_arab_pk = 0x0859, // Sindhi, Islamic Republic of Pakistan | |
| 777 | si = 0x005B, // Sinhala | |
| 778 | si_lk = 0x045B, // Sinhala, Sri Lanka | |
| 779 | sk = 0x001B, // Slovak | |
| 780 | sk_sk = 0x041B, // Slovak, Slovakia | |
| 781 | sl = 0x0024, // Slovenian | |
| 782 | sl_si = 0x0424, // Slovenian, Slovenia | |
| 783 | so = 0x0077, // Somali | |
| 784 | so_so = 0x0477, // Somali, Somalia | |
| 785 | st = 0x0030, // Sotho | |
| 786 | st_za = 0x0430, // Sotho, South Africa | |
| 787 | es = 0x000A, // Spanish | |
| 788 | es_ar = 0x2C0A, // Spanish, Argentina | |
| 789 | es_ve = 0x200A, // Spanish, Bolivarian Republic of Venezuela | |
| 790 | es_bo = 0x400A, // Spanish, Bolivia | |
| 791 | es_cl = 0x340A, // Spanish, Chile | |
| 792 | es_co = 0x240A, // Spanish, Colombia | |
| 793 | es_cr = 0x140A, // Spanish, Costa Rica | |
| 794 | es_cu = 0x5c0A, // Spanish, Cuba | |
| 795 | es_do = 0x1c0A, // Spanish, Dominican Republic | |
| 796 | es_ec = 0x300A, // Spanish, Ecuador | |
| 797 | es_sv = 0x440A, // Spanish, El Salvador | |
| 798 | es_gt = 0x100A, // Spanish, Guatemala | |
| 799 | es_hn = 0x480A, // Spanish, Honduras | |
| 800 | es_419 = 0x580A, // Spanish, Latin America | |
| 801 | es_mx = 0x080A, // Spanish, Mexico | |
| 802 | es_ni = 0x4C0A, // Spanish, Nicaragua | |
| 803 | es_pa = 0x180A, // Spanish, Panama | |
| 804 | es_py = 0x3C0A, // Spanish, Paraguay | |
| 805 | es_pe = 0x280A, // Spanish, Peru | |
| 806 | es_pr = 0x500A, // Spanish, Puerto Rico | |
| 807 | es_es_tradnl = 0x040A, // Spanish, Spain | |
| 808 | es_es = 0x0c0A, // Spanish, Spain | |
| 809 | es_us = 0x540A, // Spanish, United States | |
| 810 | es_uy = 0x380A, // Spanish, Uruguay | |
| 811 | sv = 0x001D, // Swedish | |
| 812 | sv_fi = 0x081D, // Swedish, Finland | |
| 813 | sv_se = 0x041D, // Swedish, Sweden | |
| 814 | syr = 0x005A, // Syriac | |
| 815 | syr_sy = 0x045A, // Syriac, Syria | |
| 816 | tg = 0x0028, // Tajik (Cyrillic) | |
| 817 | tg_cyrl = 0x7C28, // Tajik (Cyrillic) | |
| 818 | tg_cyrl_tj = 0x0428, // Tajik (Cyrillic), Tajikistan | |
| 819 | tzm = 0x005F, // Tamazight (Latin) | |
| 820 | tzm_latn = 0x7C5F, // Tamazight (Latin) | |
| 821 | tzm_latn_dz = 0x085F, // Tamazight (Latin), Algeria | |
| 822 | ta = 0x0049, // Tamil | |
| 823 | ta_in = 0x0449, // Tamil, India | |
| 824 | ta_lk = 0x0849, // Tamil, Sri Lanka | |
| 825 | tt = 0x0044, // Tatar | |
| 826 | tt_ru = 0x0444, // Tatar, Russia | |
| 827 | te = 0x004A, // Telugu | |
| 828 | te_in = 0x044A, // Telugu, India | |
| 829 | th = 0x001E, // Thai | |
| 830 | th_th = 0x041E, // Thai, Thailand | |
| 831 | bo = 0x0051, // Tibetan | |
| 832 | bo_cn = 0x0451, // Tibetan, People's Republic of China | |
| 833 | ti = 0x0073, // Tigrinya | |
| 834 | ti_er = 0x0873, // Tigrinya, Eritrea | |
| 835 | ti_et = 0x0473, // Tigrinya, Ethiopia | |
| 836 | ts = 0x0031, // Tsonga | |
| 837 | ts_za = 0x0431, // Tsonga, South Africa | |
| 838 | tr = 0x001F, // Turkish | |
| 839 | tr_tr = 0x041F, // Turkish, Turkey | |
| 840 | tk = 0x0042, // Turkmen | |
| 841 | tk_tm = 0x0442, // Turkmen, Turkmenistan | |
| 842 | uk = 0x0022, // Ukrainian | |
| 843 | uk_ua = 0x0422, // Ukrainian, Ukraine | |
| 844 | hsb = 0x002E, // Upper Sorbian | |
| 845 | hsb_de = 0x042E, // Upper Sorbian, Germany | |
| 846 | ur = 0x0020, // Urdu | |
| 847 | ur_in = 0x0820, // Urdu, India | |
| 848 | ur_pk = 0x0420, // Urdu, Islamic Republic of Pakistan | |
| 849 | ug = 0x0080, // Uyghur | |
| 850 | ug_cn = 0x0480, // Uyghur, People's Republic of China | |
| 851 | uz_cyrl = 0x7843, // Uzbek (Cyrillic) | |
| 852 | uz_cyrl_uz = 0x0843, // Uzbek (Cyrillic), Uzbekistan | |
| 853 | uz = 0x0043, // Uzbek (Latin) | |
| 854 | uz_latn = 0x7C43, // Uzbek (Latin) | |
| 855 | uz_latn_uz = 0x0443, // Uzbek (Latin), Uzbekistan | |
| 856 | ca_es_valencia = 0x0803, // Valencian, Spain | |
| 857 | ve = 0x0033, // Venda | |
| 858 | ve_za = 0x0433, // Venda, South Africa | |
| 859 | vi = 0x002A, // Vietnamese | |
| 860 | vi_vn = 0x042A, // Vietnamese, Vietnam | |
| 861 | cy = 0x0052, // Welsh | |
| 862 | cy_gb = 0x0452, // Welsh, United Kingdom | |
| 863 | wo = 0x0088, // Wolof | |
| 864 | wo_sn = 0x0488, // Wolof, Senegal | |
| 865 | xh = 0x0034, // Xhosa | |
| 866 | xh_za = 0x0434, // Xhosa, South Africa | |
| 867 | ii = 0x0078, // Yi | |
| 868 | ii_cn = 0x0478, // Yi, People's Republic of China | |
| 869 | yi_001 = 0x043D, // Yiddish, World | |
| 870 | yo = 0x006A, // Yoruba | |
| 871 | yo_ng = 0x046A, // Yoruba, Nigeria | |
| 872 | zu = 0x0035, // Zulu | |
| 873 | zu_za = 0x0435, // Zulu, South Africa | |
| 874 | ||
| 875 | /// Special case | |
| 876 | x_iv_mathan = 0x007F, // LANG_INVARIANT, "math alphanumeric sorting" | |
| 877 | }; |
src/resinator/lex.zig 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 | ||
| 7 | const std = @import("std"); | |
| 8 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 9 | const columnWidth = @import("literals.zig").columnWidth; | |
| 10 | const code_pages = @import("code_pages.zig"); | |
| 11 | const CodePage = code_pages.CodePage; | |
| 12 | const SourceMappings = @import("source_mapping.zig").SourceMappings; | |
| 13 | const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit; | |
| 14 | ||
| 15 | const dumpTokensDuringTests = false; | |
| 16 | ||
| 17 | pub const default_max_string_literal_codepoints = 4097; | |
| 18 | ||
| 19 | pub const Token = struct { | |
| 20 | id: Id, | |
| 21 | start: usize, | |
| 22 | end: usize, | |
| 23 | line_number: usize, | |
| 24 | ||
| 25 | pub const Id = enum { | |
| 26 | literal, | |
| 27 | number, | |
| 28 | quoted_ascii_string, | |
| 29 | quoted_wide_string, | |
| 30 | operator, | |
| 31 | begin, | |
| 32 | end, | |
| 33 | comma, | |
| 34 | open_paren, | |
| 35 | close_paren, | |
| 36 | /// This Id is only used for errors, the Lexer will never return one | |
| 37 | /// of these from a `next` call. | |
| 38 | preprocessor_command, | |
| 39 | invalid, | |
| 40 | eof, | |
| 41 | ||
| 42 | pub fn nameForErrorDisplay(self: Id) []const u8 { | |
| 43 | return switch (self) { | |
| 44 | .literal => "<literal>", | |
| 45 | .number => "<number>", | |
| 46 | .quoted_ascii_string => "<quoted ascii string>", | |
| 47 | .quoted_wide_string => "<quoted wide string>", | |
| 48 | .operator => "<operator>", | |
| 49 | .begin => "<'{' or BEGIN>", | |
| 50 | .end => "<'}' or END>", | |
| 51 | .comma => ",", | |
| 52 | .open_paren => "(", | |
| 53 | .close_paren => ")", | |
| 54 | .preprocessor_command => "<preprocessor command>", | |
| 55 | .invalid => unreachable, | |
| 56 | .eof => "<eof>", | |
| 57 | }; | |
| 58 | } | |
| 59 | }; | |
| 60 | ||
| 61 | pub fn slice(self: Token, buffer: []const u8) []const u8 { | |
| 62 | return buffer[self.start..self.end]; | |
| 63 | } | |
| 64 | ||
| 65 | pub fn nameForErrorDisplay(self: Token, buffer: []const u8) []const u8 { | |
| 66 | return switch (self.id) { | |
| 67 | .eof => self.id.nameForErrorDisplay(), | |
| 68 | else => self.slice(buffer), | |
| 69 | }; | |
| 70 | } | |
| 71 | ||
| 72 | /// 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 | ||
| 121 | pub 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 | ||
| 179 | pub 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 | ||
| 201 | pub 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 | ||
| 1037 | fn 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 | ||
| 1049 | fn expectLexError(expected: LexError, actual: anytype) !void { | |
| 1050 | try std.testing.expectError(expected, actual); | |
| 1051 | if (dumpTokensDuringTests) std.debug.print("{!}\n", .{actual}); | |
| 1052 | } | |
| 1053 | ||
| 1054 | test "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 | ||
| 1061 | test "normal: string literals" { | |
| 1062 | try testLexNormal("\"\"", &.{.quoted_ascii_string}); | |
| 1063 | // "" is an escaped " | |
| 1064 | try testLexNormal("\" \"\" \"", &.{.quoted_ascii_string}); | |
| 1065 | } | |
| 1066 | ||
| 1067 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const code_pages = @import("code_pages.zig"); | |
| 3 | const CodePage = code_pages.CodePage; | |
| 4 | const windows1252 = @import("windows1252.zig"); | |
| 5 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 6 | const DiagnosticsContext = @import("errors.zig").DiagnosticsContext; | |
| 7 | const Token = @import("lex.zig").Token; | |
| 8 | ||
| 9 | /// rc is maximally liberal in terms of what it accepts as a number literal | |
| 10 | /// for data values. As long as it starts with a number or - or ~, that's good enough. | |
| 11 | pub fn isValidNumberDataLiteral(str: []const u8) bool { | |
| 12 | if (str.len == 0) return false; | |
| 13 | switch (str[0]) { | |
| 14 | '~', '-', '0'...'9' => return true, | |
| 15 | else => return false, | |
| 16 | } | |
| 17 | } | |
| 18 | ||
| 19 | pub const SourceBytes = struct { | |
| 20 | slice: []const u8, | |
| 21 | code_page: CodePage, | |
| 22 | }; | |
| 23 | ||
| 24 | pub const StringType = enum { ascii, wide }; | |
| 25 | ||
| 26 | /// Valid escapes: | |
| 27 | /// "" -> " | |
| 28 | /// \a, \A => 0x08 (not 0x07 like in C) | |
| 29 | /// \n => 0x0A | |
| 30 | /// \r => 0x0D | |
| 31 | /// \t, \T => 0x09 | |
| 32 | /// \\ => \ | |
| 33 | /// \nnn => byte with numeric value given by nnn interpreted as octal | |
| 34 | /// (wraps on overflow, number of digits can be 1-3 for ASCII strings | |
| 35 | /// and 1-7 for wide strings) | |
| 36 | /// \xhh => byte with numeric value given by hh interpreted as hex | |
| 37 | /// (number of digits can be 0-2 for ASCII strings and 0-4 for | |
| 38 | /// wide strings) | |
| 39 | /// \<\r+> => \ | |
| 40 | /// \<[\r\n\t ]+> => <nothing> | |
| 41 | /// | |
| 42 | /// Special cases: | |
| 43 | /// <\t> => 1-8 spaces, dependent on columns in the source rc file itself | |
| 44 | /// <\r> => <nothing> | |
| 45 | /// <\n+><\w+?\n?> => <space><\n> | |
| 46 | /// | |
| 47 | /// Special, especially weird case: | |
| 48 | /// \"" => " | |
| 49 | /// NOTE: This leads to footguns because the preprocessor can start parsing things | |
| 50 | /// out-of-sync with the RC compiler, expanding macros within string literals, etc. | |
| 51 | /// This parse function handles this case the same as the Windows RC compiler, but | |
| 52 | /// \" within a string literal is treated as an error by the lexer, so the relevant | |
| 53 | /// branches should never actually be hit during this function. | |
| 54 | pub const IterativeStringParser = struct { | |
| 55 | source: []const u8, | |
| 56 | code_page: CodePage, | |
| 57 | /// The type of the string inferred by the prefix (L"" or "") | |
| 58 | /// This is what matters for things like the maximum digits in an | |
| 59 | /// escape sequence, whether or not invalid escape sequences are skipped, etc. | |
| 60 | declared_string_type: StringType, | |
| 61 | pending_codepoint: ?u21 = null, | |
| 62 | num_pending_spaces: u8 = 0, | |
| 63 | index: usize = 0, | |
| 64 | column: usize = 0, | |
| 65 | diagnostics: ?DiagnosticsContext = null, | |
| 66 | seen_tab: bool = false, | |
| 67 | ||
| 68 | const State = enum { | |
| 69 | normal, | |
| 70 | quote, | |
| 71 | newline, | |
| 72 | escaped, | |
| 73 | escaped_cr, | |
| 74 | escaped_newlines, | |
| 75 | escaped_octal, | |
| 76 | escaped_hex, | |
| 77 | }; | |
| 78 | ||
| 79 | pub fn init(bytes: SourceBytes, options: StringParseOptions) IterativeStringParser { | |
| 80 | const declared_string_type: StringType = switch (bytes.slice[0]) { | |
| 81 | 'L', 'l' => .wide, | |
| 82 | else => .ascii, | |
| 83 | }; | |
| 84 | var source = bytes.slice[1 .. bytes.slice.len - 1]; // remove "" | |
| 85 | var column = options.start_column + 1; // for the removed " | |
| 86 | if (declared_string_type == .wide) { | |
| 87 | source = source[1..]; // remove L | |
| 88 | column += 1; // for the removed L | |
| 89 | } | |
| 90 | return .{ | |
| 91 | .source = source, | |
| 92 | .code_page = bytes.code_page, | |
| 93 | .declared_string_type = declared_string_type, | |
| 94 | .column = column, | |
| 95 | .diagnostics = options.diagnostics, | |
| 96 | }; | |
| 97 | } | |
| 98 | ||
| 99 | pub const ParsedCodepoint = struct { | |
| 100 | codepoint: u21, | |
| 101 | from_escaped_integer: bool = false, | |
| 102 | }; | |
| 103 | ||
| 104 | pub fn next(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint { | |
| 105 | const result = try self.nextUnchecked(); | |
| 106 | if (self.diagnostics != null and result != null and !result.?.from_escaped_integer) { | |
| 107 | switch (result.?.codepoint) { | |
| 108 | 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => { | |
| 109 | const err: ErrorDetails.Error = if (result.?.codepoint == 0xD00) | |
| 110 | .rc_would_miscompile_codepoint_skip | |
| 111 | else | |
| 112 | .rc_would_miscompile_codepoint_byte_swap; | |
| 113 | try self.diagnostics.?.diagnostics.append(ErrorDetails{ | |
| 114 | .err = err, | |
| 115 | .type = .warning, | |
| 116 | .token = self.diagnostics.?.token, | |
| 117 | .extra = .{ .number = result.?.codepoint }, | |
| 118 | }); | |
| 119 | try self.diagnostics.?.diagnostics.append(ErrorDetails{ | |
| 120 | .err = err, | |
| 121 | .type = .note, | |
| 122 | .token = self.diagnostics.?.token, | |
| 123 | .print_source_line = false, | |
| 124 | .extra = .{ .number = result.?.codepoint }, | |
| 125 | }); | |
| 126 | }, | |
| 127 | else => {}, | |
| 128 | } | |
| 129 | } | |
| 130 | return result; | |
| 131 | } | |
| 132 | ||
| 133 | pub fn nextUnchecked(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint { | |
| 134 | if (self.num_pending_spaces > 0) { | |
| 135 | // Ensure that we don't get into this predicament so we can ensure that | |
| 136 | // the order of processing any pending stuff doesn't matter | |
| 137 | std.debug.assert(self.pending_codepoint == null); | |
| 138 | self.num_pending_spaces -= 1; | |
| 139 | return .{ .codepoint = ' ' }; | |
| 140 | } | |
| 141 | if (self.pending_codepoint) |pending_codepoint| { | |
| 142 | self.pending_codepoint = null; | |
| 143 | return .{ .codepoint = pending_codepoint }; | |
| 144 | } | |
| 145 | if (self.index >= self.source.len) return null; | |
| 146 | ||
| 147 | var state: State = .normal; | |
| 148 | var string_escape_n: u16 = 0; | |
| 149 | var string_escape_i: u8 = 0; | |
| 150 | const max_octal_escape_digits: u8 = switch (self.declared_string_type) { | |
| 151 | .ascii => 3, | |
| 152 | .wide => 7, | |
| 153 | }; | |
| 154 | const max_hex_escape_digits: u8 = switch (self.declared_string_type) { | |
| 155 | .ascii => 2, | |
| 156 | .wide => 4, | |
| 157 | }; | |
| 158 | ||
| 159 | while (self.code_page.codepointAt(self.index, self.source)) |codepoint| : (self.index += codepoint.byte_len) { | |
| 160 | const c = codepoint.value; | |
| 161 | var backtrack = false; | |
| 162 | defer { | |
| 163 | if (backtrack) { | |
| 164 | self.index -= codepoint.byte_len; | |
| 165 | } else { | |
| 166 | if (c == '\t') { | |
| 167 | self.column += columnsUntilTabStop(self.column, 8); | |
| 168 | } else { | |
| 169 | self.column += codepoint.byte_len; | |
| 170 | } | |
| 171 | } | |
| 172 | } | |
| 173 | switch (state) { | |
| 174 | .normal => switch (c) { | |
| 175 | '\\' => state = .escaped, | |
| 176 | '"' => state = .quote, | |
| 177 | '\r' => {}, | |
| 178 | '\n' => state = .newline, | |
| 179 | '\t' => { | |
| 180 | // Only warn about a tab getting converted to spaces once per string | |
| 181 | if (self.diagnostics != null and !self.seen_tab) { | |
| 182 | try self.diagnostics.?.diagnostics.append(ErrorDetails{ | |
| 183 | .err = .tab_converted_to_spaces, | |
| 184 | .type = .warning, | |
| 185 | .token = self.diagnostics.?.token, | |
| 186 | }); | |
| 187 | try self.diagnostics.?.diagnostics.append(ErrorDetails{ | |
| 188 | .err = .tab_converted_to_spaces, | |
| 189 | .type = .note, | |
| 190 | .token = self.diagnostics.?.token, | |
| 191 | .print_source_line = false, | |
| 192 | }); | |
| 193 | self.seen_tab = true; | |
| 194 | } | |
| 195 | const cols = columnsUntilTabStop(self.column, 8); | |
| 196 | self.num_pending_spaces = @intCast(cols - 1); | |
| 197 | self.index += codepoint.byte_len; | |
| 198 | return .{ .codepoint = ' ' }; | |
| 199 | }, | |
| 200 | else => { | |
| 201 | self.index += codepoint.byte_len; | |
| 202 | return .{ .codepoint = c }; | |
| 203 | }, | |
| 204 | }, | |
| 205 | .quote => switch (c) { | |
| 206 | '"' => { | |
| 207 | // "" => " | |
| 208 | self.index += codepoint.byte_len; | |
| 209 | return .{ .codepoint = '"' }; | |
| 210 | }, | |
| 211 | else => unreachable, // this is a bug in the lexer | |
| 212 | }, | |
| 213 | .newline => switch (c) { | |
| 214 | '\r', ' ', '\t', '\n', '\x0b', '\x0c', '\xa0' => {}, | |
| 215 | else => { | |
| 216 | // backtrack so that we handle the current char properly | |
| 217 | backtrack = true; | |
| 218 | // <space><newline> | |
| 219 | self.index += codepoint.byte_len; | |
| 220 | self.pending_codepoint = '\n'; | |
| 221 | return .{ .codepoint = ' ' }; | |
| 222 | }, | |
| 223 | }, | |
| 224 | .escaped => switch (c) { | |
| 225 | '\r' => state = .escaped_cr, | |
| 226 | '\n' => state = .escaped_newlines, | |
| 227 | '0'...'7' => { | |
| 228 | string_escape_n = std.fmt.charToDigit(@intCast(c), 8) catch unreachable; | |
| 229 | string_escape_i = 1; | |
| 230 | state = .escaped_octal; | |
| 231 | }, | |
| 232 | 'x', 'X' => { | |
| 233 | string_escape_n = 0; | |
| 234 | string_escape_i = 0; | |
| 235 | state = .escaped_hex; | |
| 236 | }, | |
| 237 | else => { | |
| 238 | switch (c) { | |
| 239 | 'a', 'A' => { | |
| 240 | self.index += codepoint.byte_len; | |
| 241 | return .{ .codepoint = '\x08' }; | |
| 242 | }, // might be a bug in RC, but matches its behavior | |
| 243 | 'n' => { | |
| 244 | self.index += codepoint.byte_len; | |
| 245 | return .{ .codepoint = '\n' }; | |
| 246 | }, | |
| 247 | 'r' => { | |
| 248 | self.index += codepoint.byte_len; | |
| 249 | return .{ .codepoint = '\r' }; | |
| 250 | }, | |
| 251 | 't', 'T' => { | |
| 252 | self.index += codepoint.byte_len; | |
| 253 | return .{ .codepoint = '\t' }; | |
| 254 | }, | |
| 255 | '\\' => { | |
| 256 | self.index += codepoint.byte_len; | |
| 257 | return .{ .codepoint = '\\' }; | |
| 258 | }, | |
| 259 | '"' => { | |
| 260 | // \" is a special case that doesn't get the \ included, | |
| 261 | backtrack = true; | |
| 262 | }, | |
| 263 | else => switch (self.declared_string_type) { | |
| 264 | .wide => {}, // invalid escape sequences are skipped in wide strings | |
| 265 | .ascii => { | |
| 266 | // backtrack so that we handle the current char properly | |
| 267 | backtrack = true; | |
| 268 | self.index += codepoint.byte_len; | |
| 269 | return .{ .codepoint = '\\' }; | |
| 270 | }, | |
| 271 | }, | |
| 272 | } | |
| 273 | state = .normal; | |
| 274 | }, | |
| 275 | }, | |
| 276 | .escaped_cr => switch (c) { | |
| 277 | '\r' => {}, | |
| 278 | '\n' => state = .escaped_newlines, | |
| 279 | else => { | |
| 280 | // backtrack so that we handle the current char properly | |
| 281 | backtrack = true; | |
| 282 | self.index += codepoint.byte_len; | |
| 283 | return .{ .codepoint = '\\' }; | |
| 284 | }, | |
| 285 | }, | |
| 286 | .escaped_newlines => switch (c) { | |
| 287 | '\r', '\n', '\t', ' ', '\x0b', '\x0c', '\xa0' => {}, | |
| 288 | else => { | |
| 289 | // backtrack so that we handle the current char properly | |
| 290 | backtrack = true; | |
| 291 | state = .normal; | |
| 292 | }, | |
| 293 | }, | |
| 294 | .escaped_octal => switch (c) { | |
| 295 | '0'...'7' => { | |
| 296 | string_escape_n *%= 8; | |
| 297 | string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable; | |
| 298 | string_escape_i += 1; | |
| 299 | if (string_escape_i == max_octal_escape_digits) { | |
| 300 | const escaped_value = switch (self.declared_string_type) { | |
| 301 | .ascii => @as(u8, @truncate(string_escape_n)), | |
| 302 | .wide => string_escape_n, | |
| 303 | }; | |
| 304 | self.index += codepoint.byte_len; | |
| 305 | return .{ .codepoint = escaped_value, .from_escaped_integer = true }; | |
| 306 | } | |
| 307 | }, | |
| 308 | else => { | |
| 309 | // backtrack so that we handle the current char properly | |
| 310 | backtrack = true; | |
| 311 | // write out whatever byte we have parsed so far | |
| 312 | const escaped_value = switch (self.declared_string_type) { | |
| 313 | .ascii => @as(u8, @truncate(string_escape_n)), | |
| 314 | .wide => string_escape_n, | |
| 315 | }; | |
| 316 | self.index += codepoint.byte_len; | |
| 317 | return .{ .codepoint = escaped_value, .from_escaped_integer = true }; | |
| 318 | }, | |
| 319 | }, | |
| 320 | .escaped_hex => switch (c) { | |
| 321 | '0'...'9', 'a'...'f', 'A'...'F' => { | |
| 322 | string_escape_n *= 16; | |
| 323 | string_escape_n += std.fmt.charToDigit(@intCast(c), 16) catch unreachable; | |
| 324 | string_escape_i += 1; | |
| 325 | if (string_escape_i == max_hex_escape_digits) { | |
| 326 | const escaped_value = switch (self.declared_string_type) { | |
| 327 | .ascii => @as(u8, @truncate(string_escape_n)), | |
| 328 | .wide => string_escape_n, | |
| 329 | }; | |
| 330 | self.index += codepoint.byte_len; | |
| 331 | return .{ .codepoint = escaped_value, .from_escaped_integer = true }; | |
| 332 | } | |
| 333 | }, | |
| 334 | else => { | |
| 335 | // backtrack so that we handle the current char properly | |
| 336 | backtrack = true; | |
| 337 | // write out whatever byte we have parsed so far | |
| 338 | // (even with 0 actual digits, \x alone parses to 0) | |
| 339 | const escaped_value = switch (self.declared_string_type) { | |
| 340 | .ascii => @as(u8, @truncate(string_escape_n)), | |
| 341 | .wide => string_escape_n, | |
| 342 | }; | |
| 343 | self.index += codepoint.byte_len; | |
| 344 | return .{ .codepoint = escaped_value, .from_escaped_integer = true }; | |
| 345 | }, | |
| 346 | }, | |
| 347 | } | |
| 348 | } | |
| 349 | ||
| 350 | switch (state) { | |
| 351 | .normal, .escaped_newlines => {}, | |
| 352 | .newline => { | |
| 353 | // <space><newline> | |
| 354 | self.pending_codepoint = '\n'; | |
| 355 | return .{ .codepoint = ' ' }; | |
| 356 | }, | |
| 357 | .escaped, .escaped_cr => return .{ .codepoint = '\\' }, | |
| 358 | .escaped_octal, .escaped_hex => { | |
| 359 | const escaped_value = switch (self.declared_string_type) { | |
| 360 | .ascii => @as(u8, @truncate(string_escape_n)), | |
| 361 | .wide => string_escape_n, | |
| 362 | }; | |
| 363 | return .{ .codepoint = escaped_value, .from_escaped_integer = true }; | |
| 364 | }, | |
| 365 | .quote => unreachable, // this is a bug in the lexer | |
| 366 | } | |
| 367 | ||
| 368 | return null; | |
| 369 | } | |
| 370 | }; | |
| 371 | ||
| 372 | pub const StringParseOptions = struct { | |
| 373 | start_column: usize = 0, | |
| 374 | diagnostics: ?DiagnosticsContext = null, | |
| 375 | output_code_page: CodePage = .windows1252, | |
| 376 | }; | |
| 377 | ||
| 378 | pub fn parseQuotedString( | |
| 379 | comptime literal_type: StringType, | |
| 380 | allocator: std.mem.Allocator, | |
| 381 | bytes: SourceBytes, | |
| 382 | options: StringParseOptions, | |
| 383 | ) !(switch (literal_type) { | |
| 384 | .ascii => []u8, | |
| 385 | .wide => [:0]u16, | |
| 386 | }) { | |
| 387 | const T = if (literal_type == .ascii) u8 else u16; | |
| 388 | std.debug.assert(bytes.slice.len >= 2); // must at least have 2 double quote chars | |
| 389 | ||
| 390 | var buf = try std.ArrayList(T).initCapacity(allocator, bytes.slice.len); | |
| 391 | errdefer buf.deinit(); | |
| 392 | ||
| 393 | var iterative_parser = IterativeStringParser.init(bytes, options); | |
| 394 | ||
| 395 | while (try iterative_parser.next()) |parsed| { | |
| 396 | const c = parsed.codepoint; | |
| 397 | if (parsed.from_escaped_integer) { | |
| 398 | try buf.append(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 | ||
| 446 | pub fn parseQuotedAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 { | |
| 447 | std.debug.assert(bytes.slice.len >= 2); // "" | |
| 448 | return parseQuotedString(.ascii, allocator, bytes, options); | |
| 449 | } | |
| 450 | ||
| 451 | pub fn parseQuotedWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 { | |
| 452 | std.debug.assert(bytes.slice.len >= 3); // L"" | |
| 453 | return parseQuotedString(.wide, allocator, bytes, options); | |
| 454 | } | |
| 455 | ||
| 456 | pub fn parseQuotedStringAsWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 { | |
| 457 | std.debug.assert(bytes.slice.len >= 2); // "" | |
| 458 | return parseQuotedString(.wide, allocator, bytes, options); | |
| 459 | } | |
| 460 | ||
| 461 | pub fn parseQuotedStringAsAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 { | |
| 462 | std.debug.assert(bytes.slice.len >= 2); // "" | |
| 463 | return parseQuotedString(.ascii, allocator, bytes, options); | |
| 464 | } | |
| 465 | ||
| 466 | test "parse quoted ascii string" { | |
| 467 | var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); | |
| 468 | defer arena_allocator.deinit(); | |
| 469 | const arena = arena_allocator.allocator(); | |
| 470 | ||
| 471 | try std.testing.expectEqualSlices(u8, "hello", try parseQuotedAsciiString(arena, .{ | |
| 472 | .slice = | |
| 473 | \\"hello" | |
| 474 | , | |
| 475 | .code_page = .windows1252, | |
| 476 | }, .{})); | |
| 477 | // hex with 0 digits | |
| 478 | try std.testing.expectEqualSlices(u8, "\x00", try parseQuotedAsciiString(arena, .{ | |
| 479 | .slice = | |
| 480 | \\"\x" | |
| 481 | , | |
| 482 | .code_page = .windows1252, | |
| 483 | }, .{})); | |
| 484 | // hex max of 2 digits | |
| 485 | try std.testing.expectEqualSlices(u8, "\xFFf", try parseQuotedAsciiString(arena, .{ | |
| 486 | .slice = | |
| 487 | \\"\XfFf" | |
| 488 | , | |
| 489 | .code_page = .windows1252, | |
| 490 | }, .{})); | |
| 491 | // octal with invalid octal digit | |
| 492 | try std.testing.expectEqualSlices(u8, "\x019", try parseQuotedAsciiString(arena, .{ | |
| 493 | .slice = | |
| 494 | \\"\19" | |
| 495 | , | |
| 496 | .code_page = .windows1252, | |
| 497 | }, .{})); | |
| 498 | // escaped quotes | |
| 499 | try std.testing.expectEqualSlices(u8, " \" ", try parseQuotedAsciiString(arena, .{ | |
| 500 | .slice = | |
| 501 | \\" "" " | |
| 502 | , | |
| 503 | .code_page = .windows1252, | |
| 504 | }, .{})); | |
| 505 | // backslash right before escaped quotes | |
| 506 | try std.testing.expectEqualSlices(u8, "\"", try parseQuotedAsciiString(arena, .{ | |
| 507 | .slice = | |
| 508 | \\"\""" | |
| 509 | , | |
| 510 | .code_page = .windows1252, | |
| 511 | }, .{})); | |
| 512 | // octal overflow | |
| 513 | try std.testing.expectEqualSlices(u8, "\x01", try parseQuotedAsciiString(arena, .{ | |
| 514 | .slice = | |
| 515 | \\"\401" | |
| 516 | , | |
| 517 | .code_page = .windows1252, | |
| 518 | }, .{})); | |
| 519 | // escapes | |
| 520 | try std.testing.expectEqualSlices(u8, "\x08\n\r\t\\", try parseQuotedAsciiString(arena, .{ | |
| 521 | .slice = | |
| 522 | \\"\a\n\r\t\\" | |
| 523 | , | |
| 524 | .code_page = .windows1252, | |
| 525 | }, .{})); | |
| 526 | // uppercase escapes | |
| 527 | try std.testing.expectEqualSlices(u8, "\x08\\N\\R\t\\", try parseQuotedAsciiString(arena, .{ | |
| 528 | .slice = | |
| 529 | \\"\A\N\R\T\\" | |
| 530 | , | |
| 531 | .code_page = .windows1252, | |
| 532 | }, .{})); | |
| 533 | // backslash on its own | |
| 534 | try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(arena, .{ | |
| 535 | .slice = | |
| 536 | \\"\" | |
| 537 | , | |
| 538 | .code_page = .windows1252, | |
| 539 | }, .{})); | |
| 540 | // unrecognized escapes | |
| 541 | try std.testing.expectEqualSlices(u8, "\\b", try parseQuotedAsciiString(arena, .{ | |
| 542 | .slice = | |
| 543 | \\"\b" | |
| 544 | , | |
| 545 | .code_page = .windows1252, | |
| 546 | }, .{})); | |
| 547 | // escaped carriage returns | |
| 548 | try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString( | |
| 549 | arena, | |
| 550 | .{ .slice = "\"\\\r\r\r\r\r\"", .code_page = .windows1252 }, | |
| 551 | .{}, | |
| 552 | )); | |
| 553 | // escaped newlines | |
| 554 | try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString( | |
| 555 | arena, | |
| 556 | .{ .slice = "\"\\\n\n\n\n\n\"", .code_page = .windows1252 }, | |
| 557 | .{}, | |
| 558 | )); | |
| 559 | // escaped CRLF pairs | |
| 560 | try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString( | |
| 561 | arena, | |
| 562 | .{ .slice = "\"\\\r\n\r\n\r\n\r\n\r\n\"", .code_page = .windows1252 }, | |
| 563 | .{}, | |
| 564 | )); | |
| 565 | // escaped newlines with other whitespace | |
| 566 | try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString( | |
| 567 | arena, | |
| 568 | .{ .slice = "\"\\\n \t\r\n \r\t\n \t\"", .code_page = .windows1252 }, | |
| 569 | .{}, | |
| 570 | )); | |
| 571 | // literal tab characters get converted to spaces (dependent on source file columns) | |
| 572 | try std.testing.expectEqualSlices(u8, " ", try parseQuotedAsciiString( | |
| 573 | arena, | |
| 574 | .{ .slice = "\"\t\"", .code_page = .windows1252 }, | |
| 575 | .{}, | |
| 576 | )); | |
| 577 | try std.testing.expectEqualSlices(u8, "abc ", try parseQuotedAsciiString( | |
| 578 | arena, | |
| 579 | .{ .slice = "\"abc\t\"", .code_page = .windows1252 }, | |
| 580 | .{}, | |
| 581 | )); | |
| 582 | try std.testing.expectEqualSlices(u8, "abcdefg ", try parseQuotedAsciiString( | |
| 583 | arena, | |
| 584 | .{ .slice = "\"abcdefg\t\"", .code_page = .windows1252 }, | |
| 585 | .{}, | |
| 586 | )); | |
| 587 | try std.testing.expectEqualSlices(u8, "\\ ", try parseQuotedAsciiString( | |
| 588 | arena, | |
| 589 | .{ .slice = "\"\\\t\"", .code_page = .windows1252 }, | |
| 590 | .{}, | |
| 591 | )); | |
| 592 | // literal CR's get dropped | |
| 593 | try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString( | |
| 594 | arena, | |
| 595 | .{ .slice = "\"\r\r\r\r\r\"", .code_page = .windows1252 }, | |
| 596 | .{}, | |
| 597 | )); | |
| 598 | // contiguous newlines and whitespace get collapsed to <space><newline> | |
| 599 | try std.testing.expectEqualSlices(u8, " \n", try parseQuotedAsciiString( | |
| 600 | arena, | |
| 601 | .{ .slice = "\"\n\r\r \r\n \t \"", .code_page = .windows1252 }, | |
| 602 | .{}, | |
| 603 | )); | |
| 604 | } | |
| 605 | ||
| 606 | test "parse quoted ascii string with utf8 code page" { | |
| 607 | var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); | |
| 608 | defer arena_allocator.deinit(); | |
| 609 | const arena = arena_allocator.allocator(); | |
| 610 | ||
| 611 | try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString( | |
| 612 | arena, | |
| 613 | .{ .slice = "\"\"", .code_page = .utf8 }, | |
| 614 | .{}, | |
| 615 | )); | |
| 616 | // Codepoints that don't have a Windows-1252 representation get converted to ? | |
| 617 | try std.testing.expectEqualSlices(u8, "?????????", try parseQuotedAsciiString( | |
| 618 | arena, | |
| 619 | .{ .slice = "\"кириллица\"", .code_page = .utf8 }, | |
| 620 | .{}, | |
| 621 | )); | |
| 622 | // Codepoints that have a best fit mapping get converted accordingly, | |
| 623 | // these are box drawing codepoints | |
| 624 | try std.testing.expectEqualSlices(u8, "\x2b\x2d\x2b", try parseQuotedAsciiString( | |
| 625 | arena, | |
| 626 | .{ .slice = "\"┌─┐\"", .code_page = .utf8 }, | |
| 627 | .{}, | |
| 628 | )); | |
| 629 | // Invalid UTF-8 gets converted to ? depending on well-formedness | |
| 630 | try std.testing.expectEqualSlices(u8, "????", try parseQuotedAsciiString( | |
| 631 | arena, | |
| 632 | .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 }, | |
| 633 | .{}, | |
| 634 | )); | |
| 635 | // Codepoints that would require a UTF-16 surrogate pair get converted to ?? | |
| 636 | try std.testing.expectEqualSlices(u8, "??", try parseQuotedAsciiString( | |
| 637 | arena, | |
| 638 | .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 }, | |
| 639 | .{}, | |
| 640 | )); | |
| 641 | ||
| 642 | // Output code page changes how invalid UTF-8 gets converted, since it | |
| 643 | // now encodes the result as UTF-8 so it can write replacement characters. | |
| 644 | try std.testing.expectEqualSlices(u8, "����", try parseQuotedAsciiString( | |
| 645 | arena, | |
| 646 | .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 }, | |
| 647 | .{ .output_code_page = .utf8 }, | |
| 648 | )); | |
| 649 | try std.testing.expectEqualSlices(u8, "\xF2\xAF\xBA\xB4", try parseQuotedAsciiString( | |
| 650 | arena, | |
| 651 | .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 }, | |
| 652 | .{ .output_code_page = .utf8 }, | |
| 653 | )); | |
| 654 | } | |
| 655 | ||
| 656 | test "parse quoted wide string" { | |
| 657 | var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); | |
| 658 | defer arena_allocator.deinit(); | |
| 659 | const arena = arena_allocator.allocator(); | |
| 660 | ||
| 661 | try std.testing.expectEqualSentinel(u16, 0, 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 | ||
| 715 | test "parse quoted wide string with utf8 code page" { | |
| 716 | var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); | |
| 717 | defer arena_allocator.deinit(); | |
| 718 | const arena = arena_allocator.allocator(); | |
| 719 | ||
| 720 | try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{}, try parseQuotedWideString( | |
| 721 | arena, | |
| 722 | .{ .slice = "L\"\"", .code_page = .utf8 }, | |
| 723 | .{}, | |
| 724 | )); | |
| 725 | try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedWideString( | |
| 726 | arena, | |
| 727 | .{ .slice = "L\"кириллица\"", .code_page = .utf8 }, | |
| 728 | .{}, | |
| 729 | )); | |
| 730 | // Invalid UTF-8 gets converted to � depending on well-formedness | |
| 731 | try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("����"), try parseQuotedWideString( | |
| 732 | arena, | |
| 733 | .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 }, | |
| 734 | .{}, | |
| 735 | )); | |
| 736 | } | |
| 737 | ||
| 738 | test "parse quoted ascii string as wide string" { | |
| 739 | var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); | |
| 740 | defer arena_allocator.deinit(); | |
| 741 | const arena = arena_allocator.allocator(); | |
| 742 | ||
| 743 | try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedStringAsWideString( | |
| 744 | arena, | |
| 745 | .{ .slice = "\"кириллица\"", .code_page = .utf8 }, | |
| 746 | .{}, | |
| 747 | )); | |
| 748 | // Whether or not invalid escapes are skipped is still determined by the L prefix | |
| 749 | try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("\\H"), try parseQuotedStringAsWideString( | |
| 750 | arena, | |
| 751 | .{ .slice = "\"\\H\"", .code_page = .windows1252 }, | |
| 752 | .{}, | |
| 753 | )); | |
| 754 | try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedStringAsWideString( | |
| 755 | arena, | |
| 756 | .{ .slice = "L\"\\H\"", .code_page = .windows1252 }, | |
| 757 | .{}, | |
| 758 | )); | |
| 759 | // Maximum escape sequence value is also determined by the L prefix | |
| 760 | try std.testing.expectEqualSentinel(u16, 0, &[_: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 | ||
| 772 | pub fn columnsUntilTabStop(column: usize, tab_columns: usize) usize { | |
| 773 | // 0 => 8, 1 => 7, 2 => 6, 3 => 5, 4 => 4 | |
| 774 | // 5 => 3, 6 => 2, 7 => 1, 8 => 8 | |
| 775 | return tab_columns - (column % tab_columns); | |
| 776 | } | |
| 777 | ||
| 778 | pub 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 | ||
| 785 | pub 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) | |
| 816 | pub 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 | ||
| 878 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Lexer = @import("lex.zig").Lexer; | |
| 3 | const Token = @import("lex.zig").Token; | |
| 4 | const Node = @import("ast.zig").Node; | |
| 5 | const Tree = @import("ast.zig").Tree; | |
| 6 | const CodePageLookup = @import("ast.zig").CodePageLookup; | |
| 7 | const Resource = @import("rc.zig").Resource; | |
| 8 | const Allocator = std.mem.Allocator; | |
| 9 | const ErrorDetails = @import("errors.zig").ErrorDetails; | |
| 10 | const Diagnostics = @import("errors.zig").Diagnostics; | |
| 11 | const SourceBytes = @import("literals.zig").SourceBytes; | |
| 12 | const Compiler = @import("compile.zig").Compiler; | |
| 13 | const rc = @import("rc.zig"); | |
| 14 | const res = @import("res.zig"); | |
| 15 | ||
| 16 | // TODO: Make these configurable? | |
| 17 | pub const max_nested_menu_level: u32 = 512; | |
| 18 | pub const max_nested_version_level: u32 = 512; | |
| 19 | pub const max_nested_expression_level: u32 = 200; | |
| 20 | ||
| 21 | pub const Parser = struct { | |
| 22 | const Self = @This(); | |
| 23 | ||
| 24 | lexer: *Lexer, | |
| 25 | /// values that need to be initialized per-parse | |
| 26 | state: Parser.State = undefined, | |
| 27 | options: Parser.Options, | |
| 28 | ||
| 29 | pub const Error = error{ParseError} || Allocator.Error; | |
| 30 | ||
| 31 | pub const Options = struct { | |
| 32 | warn_instead_of_error_on_invalid_code_page: bool = false, | |
| 33 | }; | |
| 34 | ||
| 35 | pub fn init(lexer: *Lexer, options: Options) Parser { | |
| 36 | return Parser{ | |
| 37 | .lexer = lexer, | |
| 38 | .options = options, | |
| 39 | }; | |
| 40 | } | |
| 41 | ||
| 42 | pub const State = struct { | |
| 43 | token: Token, | |
| 44 | lookahead_lexer: Lexer, | |
| 45 | allocator: Allocator, | |
| 46 | arena: Allocator, | |
| 47 | diagnostics: *Diagnostics, | |
| 48 | input_code_page_lookup: CodePageLookup, | |
| 49 | output_code_page_lookup: CodePageLookup, | |
| 50 | }; | |
| 51 | ||
| 52 | pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree { | |
| 53 | var arena = std.heap.ArenaAllocator.init(allocator); | |
| 54 | errdefer arena.deinit(); | |
| 55 | ||
| 56 | self.state = Parser.State{ | |
| 57 | .token = undefined, | |
| 58 | .lookahead_lexer = undefined, | |
| 59 | .allocator = allocator, | |
| 60 | .arena = arena.allocator(), | |
| 61 | .diagnostics = diagnostics, | |
| 62 | .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), | |
| 63 | .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), | |
| 64 | }; | |
| 65 | ||
| 66 | const parsed_root = try self.parseRoot(); | |
| 67 | ||
| 68 | const tree = try self.state.arena.create(Tree); | |
| 69 | tree.* = .{ | |
| 70 | .node = parsed_root, | |
| 71 | .input_code_pages = self.state.input_code_page_lookup, | |
| 72 | .output_code_pages = self.state.output_code_page_lookup, | |
| 73 | .source = self.lexer.buffer, | |
| 74 | .arena = arena.state, | |
| 75 | .allocator = allocator, | |
| 76 | }; | |
| 77 | return tree; | |
| 78 | } | |
| 79 | ||
| 80 | fn parseRoot(self: *Self) Error!*Node { | |
| 81 | var statements = std.ArrayList(*Node).init(self.state.allocator); | |
| 82 | defer statements.deinit(); | |
| 83 | ||
| 84 | try self.parseStatements(&statements); | |
| 85 | try self.check(.eof); | |
| 86 | ||
| 87 | const node = try self.state.arena.create(Node.Root); | |
| 88 | node.* = .{ | |
| 89 | .body = try self.state.arena.dupe(*Node, statements.items), | |
| 90 | }; | |
| 91 | return &node.base; | |
| 92 | } | |
| 93 | ||
| 94 | fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void { | |
| 95 | while (true) { | |
| 96 | try self.nextToken(.whitespace_delimiter_only); | |
| 97 | if (self.state.token.id == .eof) break; | |
| 98 | // The Win32 compiler will sometimes try to recover from errors | |
| 99 | // and then restart parsing afterwards. We don't ever do this | |
| 100 | // because it almost always leads to unhelpful error messages | |
| 101 | // (usually it will end up with bogus things like 'file | |
| 102 | // not found: {') | |
| 103 | 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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const Allocator = std.mem.Allocator; | |
| 4 | const cli = @import("cli.zig"); | |
| 5 | ||
| 6 | pub 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`. | |
| 23 | pub 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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const utils = @import("utils.zig"); | |
| 3 | const res = @import("res.zig"); | |
| 4 | const SourceBytes = @import("literals.zig").SourceBytes; | |
| 5 | ||
| 6 | // https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files | |
| 7 | ||
| 8 | pub const Resource = enum { | |
| 9 | accelerators, | |
| 10 | bitmap, | |
| 11 | cursor, | |
| 12 | dialog, | |
| 13 | dialogex, | |
| 14 | /// As far as I can tell, this is undocumented; the most I could find was this: | |
| 15 | /// https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/91697 | |
| 16 | dlginclude, | |
| 17 | /// Undocumented, basically works exactly like RCDATA | |
| 18 | dlginit, | |
| 19 | font, | |
| 20 | html, | |
| 21 | icon, | |
| 22 | menu, | |
| 23 | menuex, | |
| 24 | messagetable, | |
| 25 | plugplay, // Obsolete | |
| 26 | rcdata, | |
| 27 | stringtable, | |
| 28 | /// Undocumented | |
| 29 | toolbar, | |
| 30 | user_defined, | |
| 31 | versioninfo, | |
| 32 | vxd, // Obsolete | |
| 33 | ||
| 34 | // Types that are treated as a user-defined type when encountered, but have | |
| 35 | // special meaning without the Visual Studio GUI. We match the Win32 RC compiler | |
| 36 | // behavior by acting as if these keyword don't exist when compiling the .rc | |
| 37 | // (thereby treating them as user-defined). | |
| 38 | //textinclude, // A special resource that is interpreted by Visual C++. | |
| 39 | //typelib, // A special resource that is used with the /TLBID and /TLBOUT linker options | |
| 40 | ||
| 41 | // Types that can only be specified by numbers, they don't have keywords | |
| 42 | cursor_num, | |
| 43 | icon_num, | |
| 44 | string_num, | |
| 45 | anicursor_num, | |
| 46 | aniicon_num, | |
| 47 | fontdir_num, | |
| 48 | manifest_num, | |
| 49 | ||
| 50 | const map = std.ComptimeStringMapWithEql(Resource, .{ | |
| 51 | .{ "ACCELERATORS", .accelerators }, | |
| 52 | .{ "BITMAP", .bitmap }, | |
| 53 | .{ "CURSOR", .cursor }, | |
| 54 | .{ "DIALOG", .dialog }, | |
| 55 | .{ "DIALOGEX", .dialogex }, | |
| 56 | .{ "DLGINCLUDE", .dlginclude }, | |
| 57 | .{ "DLGINIT", .dlginit }, | |
| 58 | .{ "FONT", .font }, | |
| 59 | .{ "HTML", .html }, | |
| 60 | .{ "ICON", .icon }, | |
| 61 | .{ "MENU", .menu }, | |
| 62 | .{ "MENUEX", .menuex }, | |
| 63 | .{ "MESSAGETABLE", .messagetable }, | |
| 64 | .{ "PLUGPLAY", .plugplay }, | |
| 65 | .{ "RCDATA", .rcdata }, | |
| 66 | .{ "STRINGTABLE", .stringtable }, | |
| 67 | .{ "TOOLBAR", .toolbar }, | |
| 68 | .{ "VERSIONINFO", .versioninfo }, | |
| 69 | .{ "VXD", .vxd }, | |
| 70 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 71 | ||
| 72 | pub fn fromString(bytes: SourceBytes) Resource { | |
| 73 | const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes); | |
| 74 | if (maybe_ordinal) |ordinal| { | |
| 75 | if (ordinal.ordinal >= 256) return .user_defined; | |
| 76 | return fromRT(@enumFromInt(ordinal.ordinal)); | |
| 77 | } | |
| 78 | return map.get(bytes.slice) orelse .user_defined; | |
| 79 | } | |
| 80 | ||
| 81 | // TODO: Some comptime validation that RT <-> Resource conversion is synced? | |
| 82 | pub fn fromRT(rt: res.RT) Resource { | |
| 83 | return switch (rt) { | |
| 84 | .ACCELERATOR => .accelerators, | |
| 85 | .ANICURSOR => .anicursor_num, | |
| 86 | .ANIICON => .aniicon_num, | |
| 87 | .BITMAP => .bitmap, | |
| 88 | .CURSOR => .cursor_num, | |
| 89 | .DIALOG => .dialog, | |
| 90 | .DLGINCLUDE => .dlginclude, | |
| 91 | .DLGINIT => .dlginit, | |
| 92 | .FONT => .font, | |
| 93 | .FONTDIR => .fontdir_num, | |
| 94 | .GROUP_CURSOR => .cursor, | |
| 95 | .GROUP_ICON => .icon, | |
| 96 | .HTML => .html, | |
| 97 | .ICON => .icon_num, | |
| 98 | .MANIFEST => .manifest_num, | |
| 99 | .MENU => .menu, | |
| 100 | .MESSAGETABLE => .messagetable, | |
| 101 | .PLUGPLAY => .plugplay, | |
| 102 | .RCDATA => .rcdata, | |
| 103 | .STRING => .string_num, | |
| 104 | .TOOLBAR => .toolbar, | |
| 105 | .VERSION => .versioninfo, | |
| 106 | .VXD => .vxd, | |
| 107 | _ => .user_defined, | |
| 108 | }; | |
| 109 | } | |
| 110 | ||
| 111 | pub fn canUseRawData(resource: Resource) bool { | |
| 112 | return switch (resource) { | |
| 113 | .user_defined, | |
| 114 | .html, | |
| 115 | .plugplay, // Obsolete | |
| 116 | .rcdata, | |
| 117 | .vxd, // Obsolete | |
| 118 | .manifest_num, | |
| 119 | .dlginit, | |
| 120 | => true, | |
| 121 | else => false, | |
| 122 | }; | |
| 123 | } | |
| 124 | ||
| 125 | pub fn nameForErrorDisplay(resource: Resource) []const u8 { | |
| 126 | return switch (resource) { | |
| 127 | // zig fmt: off | |
| 128 | .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font, | |
| 129 | .html, .icon, .menu, .menuex, .messagetable, .plugplay, .rcdata, .stringtable, | |
| 130 | .toolbar, .versioninfo, .vxd => @tagName(resource), | |
| 131 | // zig fmt: on | |
| 132 | .user_defined => "user-defined", | |
| 133 | .cursor_num => std.fmt.comptimePrint("{d} (cursor)", .{@intFromEnum(res.RT.CURSOR)}), | |
| 134 | .icon_num => std.fmt.comptimePrint("{d} (icon)", .{@intFromEnum(res.RT.ICON)}), | |
| 135 | .string_num => std.fmt.comptimePrint("{d} (string)", .{@intFromEnum(res.RT.STRING)}), | |
| 136 | .anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@intFromEnum(res.RT.ANICURSOR)}), | |
| 137 | .aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@intFromEnum(res.RT.ANIICON)}), | |
| 138 | .fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@intFromEnum(res.RT.FONTDIR)}), | |
| 139 | .manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@intFromEnum(res.RT.MANIFEST)}), | |
| 140 | }; | |
| 141 | } | |
| 142 | }; | |
| 143 | ||
| 144 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable-resource#parameters | |
| 145 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/dialog-resource#parameters | |
| 146 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/dialogex-resource#parameters | |
| 147 | pub const OptionalStatements = enum { | |
| 148 | characteristics, | |
| 149 | language, | |
| 150 | version, | |
| 151 | ||
| 152 | // DIALOG | |
| 153 | caption, | |
| 154 | class, | |
| 155 | exstyle, | |
| 156 | font, | |
| 157 | menu, | |
| 158 | style, | |
| 159 | ||
| 160 | pub const map = std.ComptimeStringMapWithEql(OptionalStatements, .{ | |
| 161 | .{ "CHARACTERISTICS", .characteristics }, | |
| 162 | .{ "LANGUAGE", .language }, | |
| 163 | .{ "VERSION", .version }, | |
| 164 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 165 | ||
| 166 | pub const dialog_map = std.ComptimeStringMapWithEql(OptionalStatements, .{ | |
| 167 | .{ "CAPTION", .caption }, | |
| 168 | .{ "CLASS", .class }, | |
| 169 | .{ "EXSTYLE", .exstyle }, | |
| 170 | .{ "FONT", .font }, | |
| 171 | .{ "MENU", .menu }, | |
| 172 | .{ "STYLE", .style }, | |
| 173 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 174 | }; | |
| 175 | ||
| 176 | pub const Control = enum { | |
| 177 | auto3state, | |
| 178 | autocheckbox, | |
| 179 | autoradiobutton, | |
| 180 | checkbox, | |
| 181 | combobox, | |
| 182 | control, | |
| 183 | ctext, | |
| 184 | defpushbutton, | |
| 185 | edittext, | |
| 186 | hedit, | |
| 187 | iedit, | |
| 188 | groupbox, | |
| 189 | icon, | |
| 190 | listbox, | |
| 191 | ltext, | |
| 192 | pushbox, | |
| 193 | pushbutton, | |
| 194 | radiobutton, | |
| 195 | rtext, | |
| 196 | scrollbar, | |
| 197 | state3, | |
| 198 | userbutton, | |
| 199 | ||
| 200 | pub const map = std.ComptimeStringMapWithEql(Control, .{ | |
| 201 | .{ "AUTO3STATE", .auto3state }, | |
| 202 | .{ "AUTOCHECKBOX", .autocheckbox }, | |
| 203 | .{ "AUTORADIOBUTTON", .autoradiobutton }, | |
| 204 | .{ "CHECKBOX", .checkbox }, | |
| 205 | .{ "COMBOBOX", .combobox }, | |
| 206 | .{ "CONTROL", .control }, | |
| 207 | .{ "CTEXT", .ctext }, | |
| 208 | .{ "DEFPUSHBUTTON", .defpushbutton }, | |
| 209 | .{ "EDITTEXT", .edittext }, | |
| 210 | .{ "HEDIT", .hedit }, | |
| 211 | .{ "IEDIT", .iedit }, | |
| 212 | .{ "GROUPBOX", .groupbox }, | |
| 213 | .{ "ICON", .icon }, | |
| 214 | .{ "LISTBOX", .listbox }, | |
| 215 | .{ "LTEXT", .ltext }, | |
| 216 | .{ "PUSHBOX", .pushbox }, | |
| 217 | .{ "PUSHBUTTON", .pushbutton }, | |
| 218 | .{ "RADIOBUTTON", .radiobutton }, | |
| 219 | .{ "RTEXT", .rtext }, | |
| 220 | .{ "SCROLLBAR", .scrollbar }, | |
| 221 | .{ "STATE3", .state3 }, | |
| 222 | .{ "USERBUTTON", .userbutton }, | |
| 223 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 224 | ||
| 225 | pub fn hasTextParam(control: Control) bool { | |
| 226 | switch (control) { | |
| 227 | .scrollbar, .listbox, .iedit, .hedit, .edittext, .combobox => return false, | |
| 228 | else => return true, | |
| 229 | } | |
| 230 | } | |
| 231 | }; | |
| 232 | ||
| 233 | pub const ControlClass = struct { | |
| 234 | pub const map = std.ComptimeStringMapWithEql(res.ControlClass, .{ | |
| 235 | .{ "BUTTON", .button }, | |
| 236 | .{ "EDIT", .edit }, | |
| 237 | .{ "STATIC", .static }, | |
| 238 | .{ "LISTBOX", .listbox }, | |
| 239 | .{ "SCROLLBAR", .scrollbar }, | |
| 240 | .{ "COMBOBOX", .combobox }, | |
| 241 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 242 | ||
| 243 | /// Like `map.get` but works on WTF16 strings, for use with parsed | |
| 244 | /// string literals ("BUTTON", or even "\x42UTTON") | |
| 245 | pub fn fromWideString(str: []const u16) ?res.ControlClass { | |
| 246 | const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral; | |
| 247 | return if (ascii.eqlIgnoreCaseW(str, utf16Literal("BUTTON"))) | |
| 248 | .button | |
| 249 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("EDIT"))) | |
| 250 | .edit | |
| 251 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("STATIC"))) | |
| 252 | .static | |
| 253 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("LISTBOX"))) | |
| 254 | .listbox | |
| 255 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("SCROLLBAR"))) | |
| 256 | .scrollbar | |
| 257 | else if (ascii.eqlIgnoreCaseW(str, utf16Literal("COMBOBOX"))) | |
| 258 | .combobox | |
| 259 | else | |
| 260 | null; | |
| 261 | } | |
| 262 | }; | |
| 263 | ||
| 264 | const ascii = struct { | |
| 265 | /// Compares ASCII values case-insensitively, non-ASCII values are compared directly | |
| 266 | pub fn eqlIgnoreCaseW(a: []const u16, b: []const u16) bool { | |
| 267 | if (a.len != b.len) return false; | |
| 268 | for (a, b) |a_c, b_c| { | |
| 269 | if (a_c < 128) { | |
| 270 | if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false; | |
| 271 | } else { | |
| 272 | if (a_c != b_c) return false; | |
| 273 | } | |
| 274 | } | |
| 275 | return true; | |
| 276 | } | |
| 277 | }; | |
| 278 | ||
| 279 | pub const MenuItem = enum { | |
| 280 | menuitem, | |
| 281 | popup, | |
| 282 | ||
| 283 | pub const map = std.ComptimeStringMapWithEql(MenuItem, .{ | |
| 284 | .{ "MENUITEM", .menuitem }, | |
| 285 | .{ "POPUP", .popup }, | |
| 286 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 287 | ||
| 288 | pub fn isSeparator(bytes: []const u8) bool { | |
| 289 | return std.ascii.eqlIgnoreCase(bytes, "SEPARATOR"); | |
| 290 | } | |
| 291 | ||
| 292 | pub const Option = enum { | |
| 293 | checked, | |
| 294 | grayed, | |
| 295 | help, | |
| 296 | inactive, | |
| 297 | menubarbreak, | |
| 298 | menubreak, | |
| 299 | ||
| 300 | pub const map = std.ComptimeStringMapWithEql(Option, .{ | |
| 301 | .{ "CHECKED", .checked }, | |
| 302 | .{ "GRAYED", .grayed }, | |
| 303 | .{ "HELP", .help }, | |
| 304 | .{ "INACTIVE", .inactive }, | |
| 305 | .{ "MENUBARBREAK", .menubarbreak }, | |
| 306 | .{ "MENUBREAK", .menubreak }, | |
| 307 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 308 | }; | |
| 309 | }; | |
| 310 | ||
| 311 | pub const ToolbarButton = enum { | |
| 312 | button, | |
| 313 | separator, | |
| 314 | ||
| 315 | pub const map = std.ComptimeStringMapWithEql(ToolbarButton, .{ | |
| 316 | .{ "BUTTON", .button }, | |
| 317 | .{ "SEPARATOR", .separator }, | |
| 318 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 319 | }; | |
| 320 | ||
| 321 | pub const VersionInfo = enum { | |
| 322 | file_version, | |
| 323 | product_version, | |
| 324 | file_flags_mask, | |
| 325 | file_flags, | |
| 326 | file_os, | |
| 327 | file_type, | |
| 328 | file_subtype, | |
| 329 | ||
| 330 | pub const map = std.ComptimeStringMapWithEql(VersionInfo, .{ | |
| 331 | .{ "FILEVERSION", .file_version }, | |
| 332 | .{ "PRODUCTVERSION", .product_version }, | |
| 333 | .{ "FILEFLAGSMASK", .file_flags_mask }, | |
| 334 | .{ "FILEFLAGS", .file_flags }, | |
| 335 | .{ "FILEOS", .file_os }, | |
| 336 | .{ "FILETYPE", .file_type }, | |
| 337 | .{ "FILESUBTYPE", .file_subtype }, | |
| 338 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 339 | }; | |
| 340 | ||
| 341 | pub const VersionBlock = enum { | |
| 342 | block, | |
| 343 | value, | |
| 344 | ||
| 345 | pub const map = std.ComptimeStringMapWithEql(VersionBlock, .{ | |
| 346 | .{ "BLOCK", .block }, | |
| 347 | .{ "VALUE", .value }, | |
| 348 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 349 | }; | |
| 350 | ||
| 351 | /// Keywords that are be the first token in a statement and (if so) dictate how the rest | |
| 352 | /// of the statement is parsed. | |
| 353 | pub const TopLevelKeywords = enum { | |
| 354 | language, | |
| 355 | version, | |
| 356 | characteristics, | |
| 357 | stringtable, | |
| 358 | ||
| 359 | pub const map = std.ComptimeStringMapWithEql(TopLevelKeywords, .{ | |
| 360 | .{ "LANGUAGE", .language }, | |
| 361 | .{ "VERSION", .version }, | |
| 362 | .{ "CHARACTERISTICS", .characteristics }, | |
| 363 | .{ "STRINGTABLE", .stringtable }, | |
| 364 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 365 | }; | |
| 366 | ||
| 367 | pub const CommonResourceAttributes = enum { | |
| 368 | preload, | |
| 369 | loadoncall, | |
| 370 | fixed, | |
| 371 | moveable, | |
| 372 | discardable, | |
| 373 | pure, | |
| 374 | impure, | |
| 375 | shared, | |
| 376 | nonshared, | |
| 377 | ||
| 378 | pub const map = std.ComptimeStringMapWithEql(CommonResourceAttributes, .{ | |
| 379 | .{ "PRELOAD", .preload }, | |
| 380 | .{ "LOADONCALL", .loadoncall }, | |
| 381 | .{ "FIXED", .fixed }, | |
| 382 | .{ "MOVEABLE", .moveable }, | |
| 383 | .{ "DISCARDABLE", .discardable }, | |
| 384 | .{ "PURE", .pure }, | |
| 385 | .{ "IMPURE", .impure }, | |
| 386 | .{ "SHARED", .shared }, | |
| 387 | .{ "NONSHARED", .nonshared }, | |
| 388 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 389 | }; | |
| 390 | ||
| 391 | pub const AcceleratorTypeAndOptions = enum { | |
| 392 | virtkey, | |
| 393 | ascii, | |
| 394 | noinvert, | |
| 395 | alt, | |
| 396 | shift, | |
| 397 | control, | |
| 398 | ||
| 399 | pub const map = std.ComptimeStringMapWithEql(AcceleratorTypeAndOptions, .{ | |
| 400 | .{ "VIRTKEY", .virtkey }, | |
| 401 | .{ "ASCII", .ascii }, | |
| 402 | .{ "NOINVERT", .noinvert }, | |
| 403 | .{ "ALT", .alt }, | |
| 404 | .{ "SHIFT", .shift }, | |
| 405 | .{ "CONTROL", .control }, | |
| 406 | }, std.comptime_string_map.eqlAsciiIgnoreCase); | |
| 407 | }; |
src/resinator/res.zig deleted-1107| ... | ... | @@ -1,1107 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const rc = @import("rc.zig"); | |
| 3 | const Resource = rc.Resource; | |
| 4 | const CommonResourceAttributes = rc.CommonResourceAttributes; | |
| 5 | const Allocator = std.mem.Allocator; | |
| 6 | const windows1252 = @import("windows1252.zig"); | |
| 7 | const CodePage = @import("code_pages.zig").CodePage; | |
| 8 | const literals = @import("literals.zig"); | |
| 9 | const SourceBytes = literals.SourceBytes; | |
| 10 | const Codepoint = @import("code_pages.zig").Codepoint; | |
| 11 | const lang = @import("lang.zig"); | |
| 12 | const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit; | |
| 13 | ||
| 14 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types | |
| 15 | pub const RT = enum(u8) { | |
| 16 | ACCELERATOR = 9, | |
| 17 | ANICURSOR = 21, | |
| 18 | ANIICON = 22, | |
| 19 | BITMAP = 2, | |
| 20 | CURSOR = 1, | |
| 21 | DIALOG = 5, | |
| 22 | DLGINCLUDE = 17, | |
| 23 | DLGINIT = 240, | |
| 24 | FONT = 8, | |
| 25 | FONTDIR = 7, | |
| 26 | GROUP_CURSOR = 1 + 11, // CURSOR + 11 | |
| 27 | GROUP_ICON = 3 + 11, // ICON + 11 | |
| 28 | HTML = 23, | |
| 29 | ICON = 3, | |
| 30 | MANIFEST = 24, | |
| 31 | MENU = 4, | |
| 32 | MESSAGETABLE = 11, | |
| 33 | PLUGPLAY = 19, | |
| 34 | RCDATA = 10, | |
| 35 | STRING = 6, | |
| 36 | TOOLBAR = 241, | |
| 37 | VERSION = 16, | |
| 38 | VXD = 20, | |
| 39 | _, | |
| 40 | ||
| 41 | /// Returns null if the resource type is user-defined | |
| 42 | /// Asserts that the resource is not `stringtable` | |
| 43 | pub fn fromResource(resource: Resource) ?RT { | |
| 44 | return switch (resource) { | |
| 45 | .accelerators => .ACCELERATOR, | |
| 46 | .bitmap => .BITMAP, | |
| 47 | .cursor => .GROUP_CURSOR, | |
| 48 | .dialog => .DIALOG, | |
| 49 | .dialogex => .DIALOG, | |
| 50 | .dlginclude => .DLGINCLUDE, | |
| 51 | .dlginit => .DLGINIT, | |
| 52 | .font => .FONT, | |
| 53 | .html => .HTML, | |
| 54 | .icon => .GROUP_ICON, | |
| 55 | .menu => .MENU, | |
| 56 | .menuex => .MENU, | |
| 57 | .messagetable => .MESSAGETABLE, | |
| 58 | .plugplay => .PLUGPLAY, | |
| 59 | .rcdata => .RCDATA, | |
| 60 | .stringtable => unreachable, | |
| 61 | .toolbar => .TOOLBAR, | |
| 62 | .user_defined => null, | |
| 63 | .versioninfo => .VERSION, | |
| 64 | .vxd => .VXD, | |
| 65 | ||
| 66 | .cursor_num => .CURSOR, | |
| 67 | .icon_num => .ICON, | |
| 68 | .string_num => .STRING, | |
| 69 | .anicursor_num => .ANICURSOR, | |
| 70 | .aniicon_num => .ANIICON, | |
| 71 | .fontdir_num => .FONTDIR, | |
| 72 | .manifest_num => .MANIFEST, | |
| 73 | }; | |
| 74 | } | |
| 75 | }; | |
| 76 | ||
| 77 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/common-resource-attributes | |
| 78 | /// https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader | |
| 79 | pub const MemoryFlags = packed struct(u16) { | |
| 80 | value: u16, | |
| 81 | ||
| 82 | pub const MOVEABLE: u16 = 0x10; | |
| 83 | // TODO: SHARED and PURE seem to be the same thing? Testing seems to confirm this but | |
| 84 | // would like to find mention of it somewhere. | |
| 85 | pub const SHARED: u16 = 0x20; | |
| 86 | pub const PURE: u16 = 0x20; | |
| 87 | pub const PRELOAD: u16 = 0x40; | |
| 88 | pub const DISCARDABLE: u16 = 0x1000; | |
| 89 | ||
| 90 | /// Note: The defaults can have combinations that are not possible to specify within | |
| 91 | /// an .rc file, as the .rc attributes imply other values (i.e. specifying | |
| 92 | /// DISCARDABLE always implies MOVEABLE and PURE/SHARED, and yet RT_ICON | |
| 93 | /// has a default of only MOVEABLE | DISCARDABLE). | |
| 94 | pub fn defaults(predefined_resource_type: ?RT) MemoryFlags { | |
| 95 | if (predefined_resource_type == null) { | |
| 96 | return MemoryFlags{ .value = MOVEABLE | SHARED }; | |
| 97 | } else { | |
| 98 | return switch (predefined_resource_type.?) { | |
| 99 | // zig fmt: off | |
| 100 | .RCDATA, .BITMAP, .HTML, .MANIFEST, | |
| 101 | .ACCELERATOR, .VERSION, .MESSAGETABLE, | |
| 102 | .DLGINIT, .TOOLBAR, .PLUGPLAY, | |
| 103 | .VXD, => MemoryFlags{ .value = MOVEABLE | SHARED }, | |
| 104 | ||
| 105 | .GROUP_ICON, .GROUP_CURSOR, | |
| 106 | .STRING, .FONT, .DIALOG, .MENU, | |
| 107 | .DLGINCLUDE, => MemoryFlags{ .value = MOVEABLE | SHARED | DISCARDABLE }, | |
| 108 | ||
| 109 | .ICON, .CURSOR, .ANIICON, .ANICURSOR => MemoryFlags{ .value = MOVEABLE | DISCARDABLE }, | |
| 110 | .FONTDIR => MemoryFlags{ .value = MOVEABLE | PRELOAD }, | |
| 111 | // zig fmt: on | |
| 112 | // Same as predefined_resource_type == null | |
| 113 | _ => return MemoryFlags{ .value = MOVEABLE | SHARED }, | |
| 114 | }; | |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | pub fn set(self: *MemoryFlags, attribute: CommonResourceAttributes) void { | |
| 119 | switch (attribute) { | |
| 120 | .preload => self.value |= PRELOAD, | |
| 121 | .loadoncall => self.value &= ~PRELOAD, | |
| 122 | .moveable => self.value |= MOVEABLE, | |
| 123 | .fixed => self.value &= ~(MOVEABLE | DISCARDABLE), | |
| 124 | .shared => self.value |= SHARED, | |
| 125 | .nonshared => self.value &= ~(SHARED | DISCARDABLE), | |
| 126 | .pure => self.value |= PURE, | |
| 127 | .impure => self.value &= ~(PURE | DISCARDABLE), | |
| 128 | .discardable => self.value |= DISCARDABLE | MOVEABLE | PURE, | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | pub fn setGroup(self: *MemoryFlags, attribute: CommonResourceAttributes, implied_shared_or_pure: bool) void { | |
| 133 | switch (attribute) { | |
| 134 | .preload => { | |
| 135 | self.value |= PRELOAD; | |
| 136 | if (implied_shared_or_pure) self.value &= ~SHARED; | |
| 137 | }, | |
| 138 | .loadoncall => { | |
| 139 | self.value &= ~PRELOAD; | |
| 140 | if (implied_shared_or_pure) self.value |= SHARED; | |
| 141 | }, | |
| 142 | else => self.set(attribute), | |
| 143 | } | |
| 144 | } | |
| 145 | }; | |
| 146 | ||
| 147 | /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers | |
| 148 | pub const Language = packed struct(u16) { | |
| 149 | // Note: This is the default no matter what locale the current system is set to, | |
| 150 | // e.g. even if the system's locale is en-GB, en-US will still be the | |
| 151 | // default language for resources in the Win32 rc compiler. | |
| 152 | primary_language_id: u10 = lang.LANG_ENGLISH, | |
| 153 | sublanguage_id: u6 = lang.SUBLANG_ENGLISH_US, | |
| 154 | ||
| 155 | /// Default language ID as a u16 | |
| 156 | pub const default: u16 = (Language{}).asInt(); | |
| 157 | ||
| 158 | pub fn fromInt(int: u16) Language { | |
| 159 | return @bitCast(int); | |
| 160 | } | |
| 161 | ||
| 162 | pub fn asInt(self: Language) u16 { | |
| 163 | return @bitCast(self); | |
| 164 | } | |
| 165 | }; | |
| 166 | ||
| 167 | /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks | |
| 168 | pub const ControlClass = enum(u16) { | |
| 169 | button = 0x80, | |
| 170 | edit = 0x81, | |
| 171 | static = 0x82, | |
| 172 | listbox = 0x83, | |
| 173 | scrollbar = 0x84, | |
| 174 | combobox = 0x85, | |
| 175 | ||
| 176 | pub fn fromControl(control: rc.Control) ?ControlClass { | |
| 177 | return switch (control) { | |
| 178 | // zig fmt: off | |
| 179 | .auto3state, .autocheckbox, .autoradiobutton, | |
| 180 | .checkbox, .defpushbutton, .groupbox, .pushbox, | |
| 181 | .pushbutton, .radiobutton, .state3, .userbutton => .button, | |
| 182 | // zig fmt: on | |
| 183 | .combobox => .combobox, | |
| 184 | .control => null, | |
| 185 | .ctext, .icon, .ltext, .rtext => .static, | |
| 186 | .edittext, .hedit, .iedit => .edit, | |
| 187 | .listbox => .listbox, | |
| 188 | .scrollbar => .scrollbar, | |
| 189 | }; | |
| 190 | } | |
| 191 | ||
| 192 | pub fn getImpliedStyle(control: rc.Control) u32 { | |
| 193 | var style = WS.CHILD | WS.VISIBLE; | |
| 194 | switch (control) { | |
| 195 | .auto3state => style |= BS.AUTO3STATE | WS.TABSTOP, | |
| 196 | .autocheckbox => style |= BS.AUTOCHECKBOX | WS.TABSTOP, | |
| 197 | .autoradiobutton => style |= BS.AUTORADIOBUTTON, | |
| 198 | .checkbox => style |= BS.CHECKBOX | WS.TABSTOP, | |
| 199 | .combobox => {}, | |
| 200 | .control => {}, | |
| 201 | .ctext => style |= SS.CENTER | WS.GROUP, | |
| 202 | .defpushbutton => style |= BS.DEFPUSHBUTTON | WS.TABSTOP, | |
| 203 | .edittext, .hedit, .iedit => style |= WS.TABSTOP | WS.BORDER, | |
| 204 | .groupbox => style |= BS.GROUPBOX, | |
| 205 | .icon => style |= SS.ICON, | |
| 206 | .listbox => style |= LBS.NOTIFY | WS.BORDER, | |
| 207 | .ltext => style |= WS.GROUP, | |
| 208 | .pushbox => style |= BS.PUSHBOX | WS.TABSTOP, | |
| 209 | .pushbutton => style |= WS.TABSTOP, | |
| 210 | .radiobutton => style |= BS.RADIOBUTTON, | |
| 211 | .rtext => style |= SS.RIGHT | WS.GROUP, | |
| 212 | .scrollbar => {}, | |
| 213 | .state3 => style |= BS.@"3STATE" | WS.TABSTOP, | |
| 214 | .userbutton => style |= BS.USERBUTTON | WS.TABSTOP, | |
| 215 | } | |
| 216 | return style; | |
| 217 | } | |
| 218 | }; | |
| 219 | ||
| 220 | pub const NameOrOrdinal = union(enum) { | |
| 221 | // 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 | ||
| 428 | fn 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 | ||
| 441 | test "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 | ||
| 536 | test "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 | |
| 564 | pub 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 | ||
| 604 | const 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 | ||
| 617 | pub 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""). | |
| 620 | pub 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 | ||
| 698 | test "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 | ||
| 878 | pub 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 | ||
| 916 | test "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 | |
| 936 | pub 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 | ||
| 982 | test "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 | ||
| 990 | pub const VersionNode = struct { | |
| 991 | pub const type_string: u16 = 1; | |
| 992 | pub const type_binary: u16 = 0; | |
| 993 | }; | |
| 994 | ||
| 995 | pub 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 | |
| 1024 | pub 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 | |
| 1036 | pub 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 | |
| 1071 | pub 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 | |
| 1077 | pub 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 | |
| 1096 | pub 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 | |
| 1105 | pub const LBS = struct { | |
| 1106 | pub const NOTIFY: u32 = 0x0001; | |
| 1107 | }; |
src/resinator/source_mapping.zig deleted-687| ... | ... | @@ -1,687 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Allocator = std.mem.Allocator; | |
| 3 | const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter; | |
| 4 | const parseQuotedAsciiString = @import("literals.zig").parseQuotedAsciiString; | |
| 5 | const lex = @import("lex.zig"); | |
| 6 | ||
| 7 | pub const ParseLineCommandsResult = struct { | |
| 8 | result: []u8, | |
| 9 | mappings: SourceMappings, | |
| 10 | }; | |
| 11 | ||
| 12 | const CurrentMapping = struct { | |
| 13 | line_num: usize = 1, | |
| 14 | filename: std.ArrayListUnmanaged(u8) = .{}, | |
| 15 | pending: bool = true, | |
| 16 | ignore_contents: bool = false, | |
| 17 | }; | |
| 18 | ||
| 19 | pub const ParseAndRemoveLineCommandsOptions = struct { | |
| 20 | initial_filename: ?[]const u8 = null, | |
| 21 | }; | |
| 22 | ||
| 23 | /// Parses and removes #line commands as well as all source code that is within a file | |
| 24 | /// with .c or .h extensions. | |
| 25 | /// | |
| 26 | /// > RC treats files with the .c and .h extensions in a special manner. It | |
| 27 | /// > assumes that a file with one of these extensions does not contain | |
| 28 | /// > resources. If a file has the .c or .h file name extension, RC ignores all | |
| 29 | /// > lines in the file except the preprocessor directives. Therefore, to | |
| 30 | /// > include a file that contains resources in another resource script, give | |
| 31 | /// > the file to be included an extension other than .c or .h. | |
| 32 | /// from https://learn.microsoft.com/en-us/windows/win32/menurc/preprocessor-directives | |
| 33 | /// | |
| 34 | /// Returns a slice of `buf` with the aforementioned stuff removed as well as a mapping | |
| 35 | /// between the lines and their corresponding lines in their original files. | |
| 36 | /// | |
| 37 | /// `buf` must be at least as long as `source` | |
| 38 | /// In-place transformation is supported (i.e. `source` and `buf` can be the same slice) | |
| 39 | /// | |
| 40 | /// If `options.initial_filename` is provided, that filename is guaranteed to be | |
| 41 | /// within the `mappings.files` table and `root_filename_offset` will be set appropriately. | |
| 42 | pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult { | |
| 43 | var parse_result = ParseLineCommandsResult{ | |
| 44 | .result = undefined, | |
| 45 | .mappings = .{}, | |
| 46 | }; | |
| 47 | errdefer parse_result.mappings.deinit(allocator); | |
| 48 | ||
| 49 | var current_mapping: CurrentMapping = .{}; | |
| 50 | defer current_mapping.filename.deinit(allocator); | |
| 51 | ||
| 52 | if (options.initial_filename) |initial_filename| { | |
| 53 | try current_mapping.filename.appendSlice(allocator, initial_filename); | |
| 54 | parse_result.mappings.root_filename_offset = try parse_result.mappings.files.put(allocator, initial_filename); | |
| 55 | } | |
| 56 | ||
| 57 | std.debug.assert(buf.len >= source.len); | |
| 58 | var result = UncheckedSliceWriter{ .slice = buf }; | |
| 59 | const State = enum { | |
| 60 | line_start, | |
| 61 | preprocessor, | |
| 62 | non_preprocessor, | |
| 63 | }; | |
| 64 | var state: State = .line_start; | |
| 65 | var index: usize = 0; | |
| 66 | var pending_start: ?usize = null; | |
| 67 | var preprocessor_start: usize = 0; | |
| 68 | var line_number: usize = 1; | |
| 69 | while (index < source.len) : (index += 1) { | |
| 70 | const c = source[index]; | |
| 71 | switch (state) { | |
| 72 | .line_start => switch (c) { | |
| 73 | '#' => { | |
| 74 | preprocessor_start = index; | |
| 75 | state = .preprocessor; | |
| 76 | if (pending_start == null) { | |
| 77 | pending_start = index; | |
| 78 | } | |
| 79 | }, | |
| 80 | '\r', '\n' => { | |
| 81 | const is_crlf = formsLineEndingPair(source, c, index + 1); | |
| 82 | try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping); | |
| 83 | if (!current_mapping.ignore_contents) { | |
| 84 | result.write(c); | |
| 85 | if (is_crlf) result.write(source[index + 1]); | |
| 86 | line_number += 1; | |
| 87 | } | |
| 88 | if (is_crlf) index += 1; | |
| 89 | pending_start = null; | |
| 90 | }, | |
| 91 | ' ', '\t', '\x0b', '\x0c' => { | |
| 92 | if (pending_start == null) { | |
| 93 | pending_start = index; | |
| 94 | } | |
| 95 | }, | |
| 96 | else => { | |
| 97 | state = .non_preprocessor; | |
| 98 | if (pending_start != null) { | |
| 99 | if (!current_mapping.ignore_contents) { | |
| 100 | result.writeSlice(source[pending_start.? .. index + 1]); | |
| 101 | } | |
| 102 | pending_start = null; | |
| 103 | continue; | |
| 104 | } | |
| 105 | if (!current_mapping.ignore_contents) { | |
| 106 | result.write(c); | |
| 107 | } | |
| 108 | }, | |
| 109 | }, | |
| 110 | .preprocessor => switch (c) { | |
| 111 | '\r', '\n' => { | |
| 112 | // Now that we have the full line we can decide what to do with it | |
| 113 | const preprocessor_str = source[preprocessor_start..index]; | |
| 114 | const is_crlf = formsLineEndingPair(source, c, index + 1); | |
| 115 | if (std.mem.startsWith(u8, preprocessor_str, "#line")) { | |
| 116 | try handleLineCommand(allocator, preprocessor_str, &current_mapping); | |
| 117 | } else { | |
| 118 | try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping); | |
| 119 | if (!current_mapping.ignore_contents) { | |
| 120 | const line_ending_len: usize = if (is_crlf) 2 else 1; | |
| 121 | result.writeSlice(source[pending_start.? .. index + line_ending_len]); | |
| 122 | line_number += 1; | |
| 123 | } | |
| 124 | } | |
| 125 | if (is_crlf) index += 1; | |
| 126 | state = .line_start; | |
| 127 | pending_start = null; | |
| 128 | }, | |
| 129 | else => {}, | |
| 130 | }, | |
| 131 | .non_preprocessor => switch (c) { | |
| 132 | '\r', '\n' => { | |
| 133 | const is_crlf = formsLineEndingPair(source, c, index + 1); | |
| 134 | try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping); | |
| 135 | if (!current_mapping.ignore_contents) { | |
| 136 | result.write(c); | |
| 137 | if (is_crlf) result.write(source[index + 1]); | |
| 138 | line_number += 1; | |
| 139 | } | |
| 140 | if (is_crlf) index += 1; | |
| 141 | state = .line_start; | |
| 142 | pending_start = null; | |
| 143 | }, | |
| 144 | else => { | |
| 145 | if (!current_mapping.ignore_contents) { | |
| 146 | result.write(c); | |
| 147 | } | |
| 148 | }, | |
| 149 | }, | |
| 150 | } | |
| 151 | } else { | |
| 152 | switch (state) { | |
| 153 | .line_start => {}, | |
| 154 | .non_preprocessor => { | |
| 155 | try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping); | |
| 156 | }, | |
| 157 | .preprocessor => { | |
| 158 | // Now that we have the full line we can decide what to do with it | |
| 159 | const preprocessor_str = source[preprocessor_start..index]; | |
| 160 | if (std.mem.startsWith(u8, preprocessor_str, "#line")) { | |
| 161 | try handleLineCommand(allocator, preprocessor_str, &current_mapping); | |
| 162 | } else { | |
| 163 | try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping); | |
| 164 | if (!current_mapping.ignore_contents) { | |
| 165 | result.writeSlice(source[pending_start.?..index]); | |
| 166 | } | |
| 167 | } | |
| 168 | }, | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | parse_result.result = result.getWritten(); | |
| 173 | ||
| 174 | // Remove whitespace from the end of the result. This avoids issues when the | |
| 175 | // preprocessor adds a newline to the end of the file, since then the | |
| 176 | // post-preprocessed source could have more lines than the corresponding input source and | |
| 177 | // the inserted line can't be mapped to any lines in the original file. | |
| 178 | // There's no way that whitespace at the end of a file can affect the parsing | |
| 179 | // of the RC script so this is okay to do unconditionally. | |
| 180 | // TODO: There might be a better way around this | |
| 181 | while (parse_result.result.len > 0 and std.ascii.isWhitespace(parse_result.result[parse_result.result.len - 1])) { | |
| 182 | parse_result.result.len -= 1; | |
| 183 | } | |
| 184 | ||
| 185 | // If there have been no line mappings at all, then we're dealing with an empty file. | |
| 186 | // In this case, we want to fake a line mapping just so that we return something | |
| 187 | // that is useable in the same way that a non-empty mapping would be. | |
| 188 | if (parse_result.mappings.mapping.items.len == 0) { | |
| 189 | try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping); | |
| 190 | } | |
| 191 | ||
| 192 | return parse_result; | |
| 193 | } | |
| 194 | ||
| 195 | /// Note: This should function the same as lex.LineHandler.currentIndexFormsLineEndingPair | |
| 196 | pub fn formsLineEndingPair(source: []const u8, line_ending: u8, next_index: usize) bool { | |
| 197 | if (next_index >= source.len) return false; | |
| 198 | ||
| 199 | const next_ending = source[next_index]; | |
| 200 | if (next_ending != '\r' and next_ending != '\n') return false; | |
| 201 | ||
| 202 | // can't be \n\n or \r\r | |
| 203 | if (line_ending == next_ending) return false; | |
| 204 | ||
| 205 | return true; | |
| 206 | } | |
| 207 | ||
| 208 | pub fn handleLineEnd(allocator: Allocator, post_processed_line_number: usize, mapping: *SourceMappings, current_mapping: *CurrentMapping) !void { | |
| 209 | const filename_offset = try mapping.files.put(allocator, current_mapping.filename.items); | |
| 210 | ||
| 211 | try mapping.set(allocator, post_processed_line_number, .{ | |
| 212 | .start_line = current_mapping.line_num, | |
| 213 | .end_line = current_mapping.line_num, | |
| 214 | .filename_offset = filename_offset, | |
| 215 | }); | |
| 216 | ||
| 217 | current_mapping.line_num += 1; | |
| 218 | current_mapping.pending = false; | |
| 219 | } | |
| 220 | ||
| 221 | // TODO: Might want to provide diagnostics on invalid line commands instead of just returning | |
| 222 | pub fn handleLineCommand(allocator: Allocator, line_command: []const u8, current_mapping: *CurrentMapping) error{OutOfMemory}!void { | |
| 223 | // TODO: Are there other whitespace characters that should be included? | |
| 224 | var tokenizer = std.mem.tokenize(u8, line_command, " \t"); | |
| 225 | const line_directive = tokenizer.next() orelse return; // #line | |
| 226 | if (!std.mem.eql(u8, line_directive, "#line")) return; | |
| 227 | const linenum_str = tokenizer.next() orelse return; | |
| 228 | const linenum = std.fmt.parseUnsigned(usize, linenum_str, 10) catch return; | |
| 229 | ||
| 230 | var filename_literal = tokenizer.rest(); | |
| 231 | while (filename_literal.len > 0 and std.ascii.isWhitespace(filename_literal[filename_literal.len - 1])) { | |
| 232 | filename_literal.len -= 1; | |
| 233 | } | |
| 234 | if (filename_literal.len < 2) return; | |
| 235 | const is_quoted = filename_literal[0] == '"' and filename_literal[filename_literal.len - 1] == '"'; | |
| 236 | if (!is_quoted) return; | |
| 237 | const filename = parseFilename(allocator, filename_literal[1 .. filename_literal.len - 1]) catch |err| switch (err) { | |
| 238 | error.OutOfMemory => |e| return e, | |
| 239 | else => return, | |
| 240 | }; | |
| 241 | defer allocator.free(filename); | |
| 242 | ||
| 243 | // \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 | ||
| 253 | pub 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. | |
| 271 | fn 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 | ||
| 402 | fn 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 | ||
| 409 | test 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 | ||
| 423 | pub 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 | ||
| 493 | test "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 | |
| 513 | pub 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 | ||
| 557 | const ExpectedSourceSpan = struct { | |
| 558 | start_line: usize, | |
| 559 | end_line: usize, | |
| 560 | filename: []const u8, | |
| 561 | }; | |
| 562 | ||
| 563 | fn 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 | ||
| 592 | fn 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 | ||
| 604 | test "basic" { | |
| 605 | try testParseAndRemoveLineCommands("", &[_]ExpectedSourceSpan{ | |
| 606 | .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" }, | |
| 607 | }, "#line 1 \"blah.rc\"", .{}); | |
| 608 | } | |
| 609 | ||
| 610 | test "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 | ||
| 621 | test "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 | ||
| 627 | test "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 | ||
| 656 | test "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 | ||
| 669 | test "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 | ||
| 682 | test "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 @@ |
| 1 | const std = @import("std"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | ||
| 4 | /// Like std.io.FixedBufferStream but does no bounds checking | |
| 5 | pub const UncheckedSliceWriter = struct { | |
| 6 | const Self = @This(); | |
| 7 | ||
| 8 | pos: usize = 0, | |
| 9 | slice: []u8, | |
| 10 | ||
| 11 | pub fn write(self: *Self, char: u8) void { | |
| 12 | self.slice[self.pos] = char; | |
| 13 | self.pos += 1; | |
| 14 | } | |
| 15 | ||
| 16 | pub fn writeSlice(self: *Self, slice: []const u8) void { | |
| 17 | for (slice) |c| { | |
| 18 | self.write(c); | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | pub fn getWritten(self: Self) []u8 { | |
| 23 | return self.slice[0..self.pos]; | |
| 24 | } | |
| 25 | }; | |
| 26 | ||
| 27 | /// Cross-platform 'std.fs.Dir.openFile' wrapper that will always return IsDir if | |
| 28 | /// a directory is attempted to be opened. | |
| 29 | /// TODO: Remove once https://github.com/ziglang/zig/issues/5732 is addressed. | |
| 30 | pub fn openFileNotDir(cwd: std.fs.Dir, path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File { | |
| 31 | const file = try cwd.openFile(path, flags); | |
| 32 | errdefer file.close(); | |
| 33 | // https://github.com/ziglang/zig/issues/5732 | |
| 34 | if (builtin.os.tag != .windows) { | |
| 35 | const stat = try file.stat(); | |
| 36 | ||
| 37 | if (stat.kind == .directory) | |
| 38 | return error.IsDir; | |
| 39 | } | |
| 40 | return file; | |
| 41 | } | |
| 42 | ||
| 43 | /// Emulates the Windows implementation of `iswdigit`, but only returns true | |
| 44 | /// for the non-ASCII digits that `iswdigit` on Windows would return true for. | |
| 45 | pub fn isNonAsciiDigit(c: u21) bool { | |
| 46 | return switch (c) { | |
| 47 | '²', | |
| 48 | '³', | |
| 49 | '¹', | |
| 50 | '\u{660}'...'\u{669}', | |
| 51 | '\u{6F0}'...'\u{6F9}', | |
| 52 | '\u{7C0}'...'\u{7C9}', | |
| 53 | '\u{966}'...'\u{96F}', | |
| 54 | '\u{9E6}'...'\u{9EF}', | |
| 55 | '\u{A66}'...'\u{A6F}', | |
| 56 | '\u{AE6}'...'\u{AEF}', | |
| 57 | '\u{B66}'...'\u{B6F}', | |
| 58 | '\u{BE6}'...'\u{BEF}', | |
| 59 | '\u{C66}'...'\u{C6F}', | |
| 60 | '\u{CE6}'...'\u{CEF}', | |
| 61 | '\u{D66}'...'\u{D6F}', | |
| 62 | '\u{E50}'...'\u{E59}', | |
| 63 | '\u{ED0}'...'\u{ED9}', | |
| 64 | '\u{F20}'...'\u{F29}', | |
| 65 | '\u{1040}'...'\u{1049}', | |
| 66 | '\u{1090}'...'\u{1099}', | |
| 67 | '\u{17E0}'...'\u{17E9}', | |
| 68 | '\u{1810}'...'\u{1819}', | |
| 69 | '\u{1946}'...'\u{194F}', | |
| 70 | '\u{19D0}'...'\u{19D9}', | |
| 71 | '\u{1B50}'...'\u{1B59}', | |
| 72 | '\u{1BB0}'...'\u{1BB9}', | |
| 73 | '\u{1C40}'...'\u{1C49}', | |
| 74 | '\u{1C50}'...'\u{1C59}', | |
| 75 | '\u{A620}'...'\u{A629}', | |
| 76 | '\u{A8D0}'...'\u{A8D9}', | |
| 77 | '\u{A900}'...'\u{A909}', | |
| 78 | '\u{AA50}'...'\u{AA59}', | |
| 79 | '\u{FF10}'...'\u{FF19}', | |
| 80 | => true, | |
| 81 | else => false, | |
| 82 | }; | |
| 83 | } | |
| 84 | ||
| 85 | /// Used for generic colored errors/warnings/notes, more context-specific error messages | |
| 86 | /// are handled elsewhere. | |
| 87 | pub 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 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | pub fn windows1252ToUtf8Stream(writer: anytype, reader: anytype) !usize { | |
| 4 | var bytes_written: usize = 0; | |
| 5 | var utf8_buf: [3]u8 = undefined; | |
| 6 | while (true) { | |
| 7 | const c = reader.readByte() catch |err| switch (err) { | |
| 8 | error.EndOfStream => return bytes_written, | |
| 9 | else => |e| return e, | |
| 10 | }; | |
| 11 | const codepoint = toCodepoint(c); | |
| 12 | if (codepoint <= 0x7F) { | |
| 13 | try writer.writeByte(c); | |
| 14 | bytes_written += 1; | |
| 15 | } else { | |
| 16 | const utf8_len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch unreachable; | |
| 17 | try writer.writeAll(utf8_buf[0..utf8_len]); | |
| 18 | bytes_written += utf8_len; | |
| 19 | } | |
| 20 | } | |
| 21 | } | |
| 22 | ||
| 23 | /// Returns the number of code units written to the writer | |
| 24 | pub fn windows1252ToUtf16AllocZ(allocator: std.mem.Allocator, win1252_str: []const u8) ![:0]u16 { | |
| 25 | // Guaranteed to need exactly the same number of code units as Windows-1252 bytes | |
| 26 | var utf16_slice = try allocator.allocSentinel(u16, win1252_str.len, 0); | |
| 27 | errdefer allocator.free(utf16_slice); | |
| 28 | for (win1252_str, 0..) |c, i| { | |
| 29 | utf16_slice[i] = toCodepoint(c); | |
| 30 | } | |
| 31 | return utf16_slice; | |
| 32 | } | |
| 33 | ||
| 34 | /// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt | |
| 35 | pub fn toCodepoint(c: u8) u16 { | |
| 36 | return switch (c) { | |
| 37 | 0x80 => 0x20ac, // Euro Sign | |
| 38 | 0x82 => 0x201a, // Single Low-9 Quotation Mark | |
| 39 | 0x83 => 0x0192, // Latin Small Letter F With Hook | |
| 40 | 0x84 => 0x201e, // Double Low-9 Quotation Mark | |
| 41 | 0x85 => 0x2026, // Horizontal Ellipsis | |
| 42 | 0x86 => 0x2020, // Dagger | |
| 43 | 0x87 => 0x2021, // Double Dagger | |
| 44 | 0x88 => 0x02c6, // Modifier Letter Circumflex Accent | |
| 45 | 0x89 => 0x2030, // Per Mille Sign | |
| 46 | 0x8a => 0x0160, // Latin Capital Letter S With Caron | |
| 47 | 0x8b => 0x2039, // Single Left-Pointing Angle Quotation Mark | |
| 48 | 0x8c => 0x0152, // Latin Capital Ligature Oe | |
| 49 | 0x8e => 0x017d, // Latin Capital Letter Z With Caron | |
| 50 | 0x91 => 0x2018, // Left Single Quotation Mark | |
| 51 | 0x92 => 0x2019, // Right Single Quotation Mark | |
| 52 | 0x93 => 0x201c, // Left Double Quotation Mark | |
| 53 | 0x94 => 0x201d, // Right Double Quotation Mark | |
| 54 | 0x95 => 0x2022, // Bullet | |
| 55 | 0x96 => 0x2013, // En Dash | |
| 56 | 0x97 => 0x2014, // Em Dash | |
| 57 | 0x98 => 0x02dc, // Small Tilde | |
| 58 | 0x99 => 0x2122, // Trade Mark Sign | |
| 59 | 0x9a => 0x0161, // Latin Small Letter S With Caron | |
| 60 | 0x9b => 0x203a, // Single Right-Pointing Angle Quotation Mark | |
| 61 | 0x9c => 0x0153, // Latin Small Ligature Oe | |
| 62 | 0x9e => 0x017e, // Latin Small Letter Z With Caron | |
| 63 | 0x9f => 0x0178, // Latin Capital Letter Y With Diaeresis | |
| 64 | else => c, | |
| 65 | }; | |
| 66 | } | |
| 67 | ||
| 68 | /// https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt | |
| 69 | /// Plus some mappings found empirically by iterating all codepoints: | |
| 70 | /// 0x2007 => 0xA0, // Figure Space | |
| 71 | /// 0x2008 => ' ', // Punctuation Space | |
| 72 | /// 0x2009 => ' ', // Thin Space | |
| 73 | /// 0x200A => ' ', // Hair Space | |
| 74 | /// 0x2012 => '-', // Figure Dash | |
| 75 | /// 0x2015 => '-', // Horizontal Bar | |
| 76 | /// 0x201B => '\'', // Single High-reversed-9 Quotation Mark | |
| 77 | /// 0x201F => '"', // Double High-reversed-9 Quotation Mark | |
| 78 | /// 0x202F => 0xA0, // Narrow No-Break Space | |
| 79 | /// 0x2033 => '"', // Double Prime | |
| 80 | /// 0x2036 => '"', // Reversed Double Prime | |
| 81 | pub fn bestFitFromCodepoint(codepoint: u21) ?u8 { | |
| 82 | return switch (codepoint) { | |
| 83 | 0x00...0x7F, | |
| 84 | 0x81, | |
| 85 | 0x8D, | |
| 86 | 0x8F, | |
| 87 | 0x90, | |
| 88 | 0x9D, | |
| 89 | 0xA0...0xFF, | |
| 90 | => @intCast(codepoint), | |
| 91 | 0x0100 => 0x41, // Latin Capital Letter A With Macron | |
| 92 | 0x0101 => 0x61, // Latin Small Letter A With Macron | |
| 93 | 0x0102 => 0x41, // Latin Capital Letter A With Breve | |
| 94 | 0x0103 => 0x61, // Latin Small Letter A With Breve | |
| 95 | 0x0104 => 0x41, // Latin Capital Letter A With Ogonek | |
| 96 | 0x0105 => 0x61, // Latin Small Letter A With Ogonek | |
| 97 | 0x0106 => 0x43, // Latin Capital Letter C With Acute | |
| 98 | 0x0107 => 0x63, // Latin Small Letter C With Acute | |
| 99 | 0x0108 => 0x43, // Latin Capital Letter C With Circumflex | |
| 100 | 0x0109 => 0x63, // Latin Small Letter C With Circumflex | |
| 101 | 0x010a => 0x43, // Latin Capital Letter C With Dot Above | |
| 102 | 0x010b => 0x63, // Latin Small Letter C With Dot Above | |
| 103 | 0x010c => 0x43, // Latin Capital Letter C With Caron | |
| 104 | 0x010d => 0x63, // Latin Small Letter C With Caron | |
| 105 | 0x010e => 0x44, // Latin Capital Letter D With Caron | |
| 106 | 0x010f => 0x64, // Latin Small Letter D With Caron | |
| 107 | 0x0110 => 0xd0, // Latin Capital Letter D With Stroke | |
| 108 | 0x0111 => 0x64, // Latin Small Letter D With Stroke | |
| 109 | 0x0112 => 0x45, // Latin Capital Letter E With Macron | |
| 110 | 0x0113 => 0x65, // Latin Small Letter E With Macron | |
| 111 | 0x0114 => 0x45, // Latin Capital Letter E With Breve | |
| 112 | 0x0115 => 0x65, // Latin Small Letter E With Breve | |
| 113 | 0x0116 => 0x45, // Latin Capital Letter E With Dot Above | |
| 114 | 0x0117 => 0x65, // Latin Small Letter E With Dot Above | |
| 115 | 0x0118 => 0x45, // Latin Capital Letter E With Ogonek | |
| 116 | 0x0119 => 0x65, // Latin Small Letter E With Ogonek | |
| 117 | 0x011a => 0x45, // Latin Capital Letter E With Caron | |
| 118 | 0x011b => 0x65, // Latin Small Letter E With Caron | |
| 119 | 0x011c => 0x47, // Latin Capital Letter G With Circumflex | |
| 120 | 0x011d => 0x67, // Latin Small Letter G With Circumflex | |
| 121 | 0x011e => 0x47, // Latin Capital Letter G With Breve | |
| 122 | 0x011f => 0x67, // Latin Small Letter G With Breve | |
| 123 | 0x0120 => 0x47, // Latin Capital Letter G With Dot Above | |
| 124 | 0x0121 => 0x67, // Latin Small Letter G With Dot Above | |
| 125 | 0x0122 => 0x47, // Latin Capital Letter G With Cedilla | |
| 126 | 0x0123 => 0x67, // Latin Small Letter G With Cedilla | |
| 127 | 0x0124 => 0x48, // Latin Capital Letter H With Circumflex | |
| 128 | 0x0125 => 0x68, // Latin Small Letter H With Circumflex | |
| 129 | 0x0126 => 0x48, // Latin Capital Letter H With Stroke | |
| 130 | 0x0127 => 0x68, // Latin Small Letter H With Stroke | |
| 131 | 0x0128 => 0x49, // Latin Capital Letter I With Tilde | |
| 132 | 0x0129 => 0x69, // Latin Small Letter I With Tilde | |
| 133 | 0x012a => 0x49, // Latin Capital Letter I With Macron | |
| 134 | 0x012b => 0x69, // Latin Small Letter I With Macron | |
| 135 | 0x012c => 0x49, // Latin Capital Letter I With Breve | |
| 136 | 0x012d => 0x69, // Latin Small Letter I With Breve | |
| 137 | 0x012e => 0x49, // Latin Capital Letter I With Ogonek | |
| 138 | 0x012f => 0x69, // Latin Small Letter I With Ogonek | |
| 139 | 0x0130 => 0x49, // Latin Capital Letter I With Dot Above | |
| 140 | 0x0131 => 0x69, // Latin Small Letter Dotless I | |
| 141 | 0x0134 => 0x4a, // Latin Capital Letter J With Circumflex | |
| 142 | 0x0135 => 0x6a, // Latin Small Letter J With Circumflex | |
| 143 | 0x0136 => 0x4b, // Latin Capital Letter K With Cedilla | |
| 144 | 0x0137 => 0x6b, // Latin Small Letter K With Cedilla | |
| 145 | 0x0139 => 0x4c, // Latin Capital Letter L With Acute | |
| 146 | 0x013a => 0x6c, // Latin Small Letter L With Acute | |
| 147 | 0x013b => 0x4c, // Latin Capital Letter L With Cedilla | |
| 148 | 0x013c => 0x6c, // Latin Small Letter L With Cedilla | |
| 149 | 0x013d => 0x4c, // Latin Capital Letter L With Caron | |
| 150 | 0x013e => 0x6c, // Latin Small Letter L With Caron | |
| 151 | 0x0141 => 0x4c, // Latin Capital Letter L With Stroke | |
| 152 | 0x0142 => 0x6c, // Latin Small Letter L With Stroke | |
| 153 | 0x0143 => 0x4e, // Latin Capital Letter N With Acute | |
| 154 | 0x0144 => 0x6e, // Latin Small Letter N With Acute | |
| 155 | 0x0145 => 0x4e, // Latin Capital Letter N With Cedilla | |
| 156 | 0x0146 => 0x6e, // Latin Small Letter N With Cedilla | |
| 157 | 0x0147 => 0x4e, // Latin Capital Letter N With Caron | |
| 158 | 0x0148 => 0x6e, // Latin Small Letter N With Caron | |
| 159 | 0x014c => 0x4f, // Latin Capital Letter O With Macron | |
| 160 | 0x014d => 0x6f, // Latin Small Letter O With Macron | |
| 161 | 0x014e => 0x4f, // Latin Capital Letter O With Breve | |
| 162 | 0x014f => 0x6f, // Latin Small Letter O With Breve | |
| 163 | 0x0150 => 0x4f, // Latin Capital Letter O With Double Acute | |
| 164 | 0x0151 => 0x6f, // Latin Small Letter O With Double Acute | |
| 165 | 0x0152 => 0x8c, // Latin Capital Ligature Oe | |
| 166 | 0x0153 => 0x9c, // Latin Small Ligature Oe | |
| 167 | 0x0154 => 0x52, // Latin Capital Letter R With Acute | |
| 168 | 0x0155 => 0x72, // Latin Small Letter R With Acute | |
| 169 | 0x0156 => 0x52, // Latin Capital Letter R With Cedilla | |
| 170 | 0x0157 => 0x72, // Latin Small Letter R With Cedilla | |
| 171 | 0x0158 => 0x52, // Latin Capital Letter R With Caron | |
| 172 | 0x0159 => 0x72, // Latin Small Letter R With Caron | |
| 173 | 0x015a => 0x53, // Latin Capital Letter S With Acute | |
| 174 | 0x015b => 0x73, // Latin Small Letter S With Acute | |
| 175 | 0x015c => 0x53, // Latin Capital Letter S With Circumflex | |
| 176 | 0x015d => 0x73, // Latin Small Letter S With Circumflex | |
| 177 | 0x015e => 0x53, // Latin Capital Letter S With Cedilla | |
| 178 | 0x015f => 0x73, // Latin Small Letter S With Cedilla | |
| 179 | 0x0160 => 0x8a, // Latin Capital Letter S With Caron | |
| 180 | 0x0161 => 0x9a, // Latin Small Letter S With Caron | |
| 181 | 0x0162 => 0x54, // Latin Capital Letter T With Cedilla | |
| 182 | 0x0163 => 0x74, // Latin Small Letter T With Cedilla | |
| 183 | 0x0164 => 0x54, // Latin Capital Letter T With Caron | |
| 184 | 0x0165 => 0x74, // Latin Small Letter T With Caron | |
| 185 | 0x0166 => 0x54, // Latin Capital Letter T With Stroke | |
| 186 | 0x0167 => 0x74, // Latin Small Letter T With Stroke | |
| 187 | 0x0168 => 0x55, // Latin Capital Letter U With Tilde | |
| 188 | 0x0169 => 0x75, // Latin Small Letter U With Tilde | |
| 189 | 0x016a => 0x55, // Latin Capital Letter U With Macron | |
| 190 | 0x016b => 0x75, // Latin Small Letter U With Macron | |
| 191 | 0x016c => 0x55, // Latin Capital Letter U With Breve | |
| 192 | 0x016d => 0x75, // Latin Small Letter U With Breve | |
| 193 | 0x016e => 0x55, // Latin Capital Letter U With Ring Above | |
| 194 | 0x016f => 0x75, // Latin Small Letter U With Ring Above | |
| 195 | 0x0170 => 0x55, // Latin Capital Letter U With Double Acute | |
| 196 | 0x0171 => 0x75, // Latin Small Letter U With Double Acute | |
| 197 | 0x0172 => 0x55, // Latin Capital Letter U With Ogonek | |
| 198 | 0x0173 => 0x75, // Latin Small Letter U With Ogonek | |
| 199 | 0x0174 => 0x57, // Latin Capital Letter W With Circumflex | |
| 200 | 0x0175 => 0x77, // Latin Small Letter W With Circumflex | |
| 201 | 0x0176 => 0x59, // Latin Capital Letter Y With Circumflex | |
| 202 | 0x0177 => 0x79, // Latin Small Letter Y With Circumflex | |
| 203 | 0x0178 => 0x9f, // Latin Capital Letter Y With Diaeresis | |
| 204 | 0x0179 => 0x5a, // Latin Capital Letter Z With Acute | |
| 205 | 0x017a => 0x7a, // Latin Small Letter Z With Acute | |
| 206 | 0x017b => 0x5a, // Latin Capital Letter Z With Dot Above | |
| 207 | 0x017c => 0x7a, // Latin Small Letter Z With Dot Above | |
| 208 | 0x017d => 0x8e, // Latin Capital Letter Z With Caron | |
| 209 | 0x017e => 0x9e, // Latin Small Letter Z With Caron | |
| 210 | 0x0180 => 0x62, // Latin Small Letter B With Stroke | |
| 211 | 0x0189 => 0xd0, // Latin Capital Letter African D | |
| 212 | 0x0191 => 0x83, // Latin Capital Letter F With Hook | |
| 213 | 0x0192 => 0x83, // Latin Small Letter F With Hook | |
| 214 | 0x0197 => 0x49, // Latin Capital Letter I With Stroke | |
| 215 | 0x019a => 0x6c, // Latin Small Letter L With Bar | |
| 216 | 0x019f => 0x4f, // Latin Capital Letter O With Middle Tilde | |
| 217 | 0x01a0 => 0x4f, // Latin Capital Letter O With Horn | |
| 218 | 0x01a1 => 0x6f, // Latin Small Letter O With Horn | |
| 219 | 0x01ab => 0x74, // Latin Small Letter T With Palatal Hook | |
| 220 | 0x01ae => 0x54, // Latin Capital Letter T With Retroflex Hook | |
| 221 | 0x01af => 0x55, // Latin Capital Letter U With Horn | |
| 222 | 0x01b0 => 0x75, // Latin Small Letter U With Horn | |
| 223 | 0x01b6 => 0x7a, // Latin Small Letter Z With Stroke | |
| 224 | 0x01c0 => 0x7c, // Latin Letter Dental Click | |
| 225 | 0x01c3 => 0x21, // Latin Letter Retroflex Click | |
| 226 | 0x01cd => 0x41, // Latin Capital Letter A With Caron | |
| 227 | 0x01ce => 0x61, // Latin Small Letter A With Caron | |
| 228 | 0x01cf => 0x49, // Latin Capital Letter I With Caron | |
| 229 | 0x01d0 => 0x69, // Latin Small Letter I With Caron | |
| 230 | 0x01d1 => 0x4f, // Latin Capital Letter O With Caron | |
| 231 | 0x01d2 => 0x6f, // Latin Small Letter O With Caron | |
| 232 | 0x01d3 => 0x55, // Latin Capital Letter U With Caron | |
| 233 | 0x01d4 => 0x75, // Latin Small Letter U With Caron | |
| 234 | 0x01d5 => 0x55, // Latin Capital Letter U With Diaeresis And Macron | |
| 235 | 0x01d6 => 0x75, // Latin Small Letter U With Diaeresis And Macron | |
| 236 | 0x01d7 => 0x55, // Latin Capital Letter U With Diaeresis And Acute | |
| 237 | 0x01d8 => 0x75, // Latin Small Letter U With Diaeresis And Acute | |
| 238 | 0x01d9 => 0x55, // Latin Capital Letter U With Diaeresis And Caron | |
| 239 | 0x01da => 0x75, // Latin Small Letter U With Diaeresis And Caron | |
| 240 | 0x01db => 0x55, // Latin Capital Letter U With Diaeresis And Grave | |
| 241 | 0x01dc => 0x75, // Latin Small Letter U With Diaeresis And Grave | |
| 242 | 0x01de => 0x41, // Latin Capital Letter A With Diaeresis And Macron | |
| 243 | 0x01df => 0x61, // Latin Small Letter A With Diaeresis And Macron | |
| 244 | 0x01e4 => 0x47, // Latin Capital Letter G With Stroke | |
| 245 | 0x01e5 => 0x67, // Latin Small Letter G With Stroke | |
| 246 | 0x01e6 => 0x47, // Latin Capital Letter G With Caron | |
| 247 | 0x01e7 => 0x67, // Latin Small Letter G With Caron | |
| 248 | 0x01e8 => 0x4b, // Latin Capital Letter K With Caron | |
| 249 | 0x01e9 => 0x6b, // Latin Small Letter K With Caron | |
| 250 | 0x01ea => 0x4f, // Latin Capital Letter O With Ogonek | |
| 251 | 0x01eb => 0x6f, // Latin Small Letter O With Ogonek | |
| 252 | 0x01ec => 0x4f, // Latin Capital Letter O With Ogonek And Macron | |
| 253 | 0x01ed => 0x6f, // Latin Small Letter O With Ogonek And Macron | |
| 254 | 0x01f0 => 0x6a, // Latin Small Letter J With Caron | |
| 255 | 0x0261 => 0x67, // Latin Small Letter Script G | |
| 256 | 0x02b9 => 0x27, // Modifier Letter Prime | |
| 257 | 0x02ba => 0x22, // Modifier Letter Double Prime | |
| 258 | 0x02bc => 0x27, // Modifier Letter Apostrophe | |
| 259 | 0x02c4 => 0x5e, // Modifier Letter Up Arrowhead | |
| 260 | 0x02c6 => 0x88, // Modifier Letter Circumflex Accent | |
| 261 | 0x02c8 => 0x27, // Modifier Letter Vertical Line | |
| 262 | 0x02c9 => 0xaf, // Modifier Letter Macron | |
| 263 | 0x02ca => 0xb4, // Modifier Letter Acute Accent | |
| 264 | 0x02cb => 0x60, // Modifier Letter Grave Accent | |
| 265 | 0x02cd => 0x5f, // Modifier Letter Low Macron | |
| 266 | 0x02da => 0xb0, // Ring Above | |
| 267 | 0x02dc => 0x98, // Small Tilde | |
| 268 | 0x0300 => 0x60, // Combining Grave Accent | |
| 269 | 0x0301 => 0xb4, // Combining Acute Accent | |
| 270 | 0x0302 => 0x5e, // Combining Circumflex Accent | |
| 271 | 0x0303 => 0x7e, // Combining Tilde | |
| 272 | 0x0304 => 0xaf, // Combining Macron | |
| 273 | 0x0305 => 0xaf, // Combining Overline | |
| 274 | 0x0308 => 0xa8, // Combining Diaeresis | |
| 275 | 0x030a => 0xb0, // Combining Ring Above | |
| 276 | 0x030e => 0x22, // Combining Double Vertical Line Above | |
| 277 | 0x0327 => 0xb8, // Combining Cedilla | |
| 278 | 0x0331 => 0x5f, // Combining Macron Below | |
| 279 | 0x0332 => 0x5f, // Combining Low Line | |
| 280 | 0x037e => 0x3b, // Greek Question Mark | |
| 281 | 0x0393 => 0x47, // Greek Capital Letter Gamma | |
| 282 | 0x0398 => 0x54, // Greek Capital Letter Theta | |
| 283 | 0x03a3 => 0x53, // Greek Capital Letter Sigma | |
| 284 | 0x03a6 => 0x46, // Greek Capital Letter Phi | |
| 285 | 0x03a9 => 0x4f, // Greek Capital Letter Omega | |
| 286 | 0x03b1 => 0x61, // Greek Small Letter Alpha | |
| 287 | 0x03b2 => 0xdf, // Greek Small Letter Beta | |
| 288 | 0x03b4 => 0x64, // Greek Small Letter Delta | |
| 289 | 0x03b5 => 0x65, // Greek Small Letter Epsilon | |
| 290 | 0x03bc => 0xb5, // Greek Small Letter Mu | |
| 291 | 0x03c0 => 0x70, // Greek Small Letter Pi | |
| 292 | 0x03c3 => 0x73, // Greek Small Letter Sigma | |
| 293 | 0x03c4 => 0x74, // Greek Small Letter Tau | |
| 294 | 0x03c6 => 0x66, // Greek Small Letter Phi | |
| 295 | 0x04bb => 0x68, // Cyrillic Small Letter Shha | |
| 296 | 0x0589 => 0x3a, // Armenian Full Stop | |
| 297 | 0x066a => 0x25, // Arabic Percent Sign | |
| 298 | 0x2000 => 0x20, // En Quad | |
| 299 | 0x2001 => 0x20, // Em Quad | |
| 300 | 0x2002 => 0x20, // En Space | |
| 301 | 0x2003 => 0x20, // Em Space | |
| 302 | 0x2004 => 0x20, // Three-Per-Em Space | |
| 303 | 0x2005 => 0x20, // Four-Per-Em Space | |
| 304 | 0x2006 => 0x20, // Six-Per-Em Space | |
| 305 | 0x2010 => 0x2d, // Hyphen | |
| 306 | 0x2011 => 0x2d, // Non-Breaking Hyphen | |
| 307 | 0x2013 => 0x96, // En Dash | |
| 308 | 0x2014 => 0x97, // Em Dash | |
| 309 | 0x2017 => 0x3d, // Double Low Line | |
| 310 | 0x2018 => 0x91, // Left Single Quotation Mark | |
| 311 | 0x2019 => 0x92, // Right Single Quotation Mark | |
| 312 | 0x201a => 0x82, // Single Low-9 Quotation Mark | |
| 313 | 0x201c => 0x93, // Left Double Quotation Mark | |
| 314 | 0x201d => 0x94, // Right Double Quotation Mark | |
| 315 | 0x201e => 0x84, // Double Low-9 Quotation Mark | |
| 316 | 0x2020 => 0x86, // Dagger | |
| 317 | 0x2021 => 0x87, // Double Dagger | |
| 318 | 0x2022 => 0x95, // Bullet | |
| 319 | 0x2024 => 0xb7, // One Dot Leader | |
| 320 | 0x2026 => 0x85, // Horizontal Ellipsis | |
| 321 | 0x2030 => 0x89, // Per Mille Sign | |
| 322 | 0x2032 => 0x27, // Prime | |
| 323 | 0x2035 => 0x60, // Reversed Prime | |
| 324 | 0x2039 => 0x8b, // Single Left-Pointing Angle Quotation Mark | |
| 325 | 0x203a => 0x9b, // Single Right-Pointing Angle Quotation Mark | |
| 326 | 0x2044 => 0x2f, // Fraction Slash | |
| 327 | 0x2070 => 0xb0, // Superscript Zero | |
| 328 | 0x2074 => 0x34, // Superscript Four | |
| 329 | 0x2075 => 0x35, // Superscript Five | |
| 330 | 0x2076 => 0x36, // Superscript Six | |
| 331 | 0x2077 => 0x37, // Superscript Seven | |
| 332 | 0x2078 => 0x38, // Superscript Eight | |
| 333 | 0x207f => 0x6e, // Superscript Latin Small Letter N | |
| 334 | 0x2080 => 0x30, // Subscript Zero | |
| 335 | 0x2081 => 0x31, // Subscript One | |
| 336 | 0x2082 => 0x32, // Subscript Two | |
| 337 | 0x2083 => 0x33, // Subscript Three | |
| 338 | 0x2084 => 0x34, // Subscript Four | |
| 339 | 0x2085 => 0x35, // Subscript Five | |
| 340 | 0x2086 => 0x36, // Subscript Six | |
| 341 | 0x2087 => 0x37, // Subscript Seven | |
| 342 | 0x2088 => 0x38, // Subscript Eight | |
| 343 | 0x2089 => 0x39, // Subscript Nine | |
| 344 | 0x20ac => 0x80, // Euro Sign | |
| 345 | 0x20a1 => 0xa2, // Colon Sign | |
| 346 | 0x20a4 => 0xa3, // Lira Sign | |
| 347 | 0x20a7 => 0x50, // Peseta Sign | |
| 348 | 0x2102 => 0x43, // Double-Struck Capital C | |
| 349 | 0x2107 => 0x45, // Euler Constant | |
| 350 | 0x210a => 0x67, // Script Small G | |
| 351 | 0x210b => 0x48, // Script Capital H | |
| 352 | 0x210c => 0x48, // Black-Letter Capital H | |
| 353 | 0x210d => 0x48, // Double-Struck Capital H | |
| 354 | 0x210e => 0x68, // Planck Constant | |
| 355 | 0x2110 => 0x49, // Script Capital I | |
| 356 | 0x2111 => 0x49, // Black-Letter Capital I | |
| 357 | 0x2112 => 0x4c, // Script Capital L | |
| 358 | 0x2113 => 0x6c, // Script Small L | |
| 359 | 0x2115 => 0x4e, // Double-Struck Capital N | |
| 360 | 0x2118 => 0x50, // Script Capital P | |
| 361 | 0x2119 => 0x50, // Double-Struck Capital P | |
| 362 | 0x211a => 0x51, // Double-Struck Capital Q | |
| 363 | 0x211b => 0x52, // Script Capital R | |
| 364 | 0x211c => 0x52, // Black-Letter Capital R | |
| 365 | 0x211d => 0x52, // Double-Struck Capital R | |
| 366 | 0x2122 => 0x99, // Trade Mark Sign | |
| 367 | 0x2124 => 0x5a, // Double-Struck Capital Z | |
| 368 | 0x2128 => 0x5a, // Black-Letter Capital Z | |
| 369 | 0x212a => 0x4b, // Kelvin Sign | |
| 370 | 0x212b => 0xc5, // Angstrom Sign | |
| 371 | 0x212c => 0x42, // Script Capital B | |
| 372 | 0x212d => 0x43, // Black-Letter Capital C | |
| 373 | 0x212e => 0x65, // Estimated Symbol | |
| 374 | 0x212f => 0x65, // Script Small E | |
| 375 | 0x2130 => 0x45, // Script Capital E | |
| 376 | 0x2131 => 0x46, // Script Capital F | |
| 377 | 0x2133 => 0x4d, // Script Capital M | |
| 378 | 0x2134 => 0x6f, // Script Small O | |
| 379 | 0x2205 => 0xd8, // Empty Set | |
| 380 | 0x2212 => 0x2d, // Minus Sign | |
| 381 | 0x2213 => 0xb1, // Minus-Or-Plus Sign | |
| 382 | 0x2215 => 0x2f, // Division Slash | |
| 383 | 0x2216 => 0x5c, // Set Minus | |
| 384 | 0x2217 => 0x2a, // Asterisk Operator | |
| 385 | 0x2218 => 0xb0, // Ring Operator | |
| 386 | 0x2219 => 0xb7, // Bullet Operator | |
| 387 | 0x221a => 0x76, // Square Root | |
| 388 | 0x221e => 0x38, // Infinity | |
| 389 | 0x2223 => 0x7c, // Divides | |
| 390 | 0x2229 => 0x6e, // Intersection | |
| 391 | 0x2236 => 0x3a, // Ratio | |
| 392 | 0x223c => 0x7e, // Tilde Operator | |
| 393 | 0x2248 => 0x98, // Almost Equal To | |
| 394 | 0x2261 => 0x3d, // Identical To | |
| 395 | 0x2264 => 0x3d, // Less-Than Or Equal To | |
| 396 | 0x2265 => 0x3d, // Greater-Than Or Equal To | |
| 397 | 0x226a => 0xab, // Much Less-Than | |
| 398 | 0x226b => 0xbb, // Much Greater-Than | |
| 399 | 0x22c5 => 0xb7, // Dot Operator | |
| 400 | 0x2302 => 0xa6, // House | |
| 401 | 0x2303 => 0x5e, // Up Arrowhead | |
| 402 | 0x2310 => 0xac, // Reversed Not Sign | |
| 403 | 0x2320 => 0x28, // Top Half Integral | |
| 404 | 0x2321 => 0x29, // Bottom Half Integral | |
| 405 | 0x2329 => 0x3c, // Left-Pointing Angle Bracket | |
| 406 | 0x232a => 0x3e, // Right-Pointing Angle Bracket | |
| 407 | 0x2500 => 0x2d, // Box Drawings Light Horizontal | |
| 408 | 0x2502 => 0xa6, // Box Drawings Light Vertical | |
| 409 | 0x250c => 0x2b, // Box Drawings Light Down And Right | |
| 410 | 0x2510 => 0x2b, // Box Drawings Light Down And Left | |
| 411 | 0x2514 => 0x2b, // Box Drawings Light Up And Right | |
| 412 | 0x2518 => 0x2b, // Box Drawings Light Up And Left | |
| 413 | 0x251c => 0x2b, // Box Drawings Light Vertical And Right | |
| 414 | 0x2524 => 0xa6, // Box Drawings Light Vertical And Left | |
| 415 | 0x252c => 0x2d, // Box Drawings Light Down And Horizontal | |
| 416 | 0x2534 => 0x2d, // Box Drawings Light Up And Horizontal | |
| 417 | 0x253c => 0x2b, // Box Drawings Light Vertical And Horizontal | |
| 418 | 0x2550 => 0x2d, // Box Drawings Double Horizontal | |
| 419 | 0x2551 => 0xa6, // Box Drawings Double Vertical | |
| 420 | 0x2552 => 0x2b, // Box Drawings Down Single And Right Double | |
| 421 | 0x2553 => 0x2b, // Box Drawings Down Double And Right Single | |
| 422 | 0x2554 => 0x2b, // Box Drawings Double Down And Right | |
| 423 | 0x2555 => 0x2b, // Box Drawings Down Single And Left Double | |
| 424 | 0x2556 => 0x2b, // Box Drawings Down Double And Left Single | |
| 425 | 0x2557 => 0x2b, // Box Drawings Double Down And Left | |
| 426 | 0x2558 => 0x2b, // Box Drawings Up Single And Right Double | |
| 427 | 0x2559 => 0x2b, // Box Drawings Up Double And Right Single | |
| 428 | 0x255a => 0x2b, // Box Drawings Double Up And Right | |
| 429 | 0x255b => 0x2b, // Box Drawings Up Single And Left Double | |
| 430 | 0x255c => 0x2b, // Box Drawings Up Double And Left Single | |
| 431 | 0x255d => 0x2b, // Box Drawings Double Up And Left | |
| 432 | 0x255e => 0xa6, // Box Drawings Vertical Single And Right Double | |
| 433 | 0x255f => 0xa6, // Box Drawings Vertical Double And Right Single | |
| 434 | 0x2560 => 0xa6, // Box Drawings Double Vertical And Right | |
| 435 | 0x2561 => 0xa6, // Box Drawings Vertical Single And Left Double | |
| 436 | 0x2562 => 0xa6, // Box Drawings Vertical Double And Left Single | |
| 437 | 0x2563 => 0xa6, // Box Drawings Double Vertical And Left | |
| 438 | 0x2564 => 0x2d, // Box Drawings Down Single And Horizontal Double | |
| 439 | 0x2565 => 0x2d, // Box Drawings Down Double And Horizontal Single | |
| 440 | 0x2566 => 0x2d, // Box Drawings Double Down And Horizontal | |
| 441 | 0x2567 => 0x2d, // Box Drawings Up Single And Horizontal Double | |
| 442 | 0x2568 => 0x2d, // Box Drawings Up Double And Horizontal Single | |
| 443 | 0x2569 => 0x2d, // Box Drawings Double Up And Horizontal | |
| 444 | 0x256a => 0x2b, // Box Drawings Vertical Single And Horizontal Double | |
| 445 | 0x256b => 0x2b, // Box Drawings Vertical Double And Horizontal Single | |
| 446 | 0x256c => 0x2b, // Box Drawings Double Vertical And Horizontal | |
| 447 | 0x2580 => 0xaf, // Upper Half Block | |
| 448 | 0x2584 => 0x5f, // Lower Half Block | |
| 449 | 0x2588 => 0xa6, // Full Block | |
| 450 | 0x258c => 0xa6, // Left Half Block | |
| 451 | 0x2590 => 0xa6, // Right Half Block | |
| 452 | 0x2591 => 0xa6, // Light Shade | |
| 453 | 0x2592 => 0xa6, // Medium Shade | |
| 454 | 0x2593 => 0xa6, // Dark Shade | |
| 455 | 0x25a0 => 0xa6, // Black Square | |
| 456 | 0x263c => 0xa4, // White Sun With Rays | |
| 457 | 0x2758 => 0x7c, // Light Vertical Bar | |
| 458 | 0x3000 => 0x20, // Ideographic Space | |
| 459 | 0x3008 => 0x3c, // Left Angle Bracket | |
| 460 | 0x3009 => 0x3e, // Right Angle Bracket | |
| 461 | 0x300a => 0xab, // Left Double Angle Bracket | |
| 462 | 0x300b => 0xbb, // Right Double Angle Bracket | |
| 463 | 0x301a => 0x5b, // Left White Square Bracket | |
| 464 | 0x301b => 0x5d, // Right White Square Bracket | |
| 465 | 0x30fb => 0xb7, // Katakana Middle Dot | |
| 466 | 0xff01 => 0x21, // Fullwidth Exclamation Mark | |
| 467 | 0xff02 => 0x22, // Fullwidth Quotation Mark | |
| 468 | 0xff03 => 0x23, // Fullwidth Number Sign | |
| 469 | 0xff04 => 0x24, // Fullwidth Dollar Sign | |
| 470 | 0xff05 => 0x25, // Fullwidth Percent Sign | |
| 471 | 0xff06 => 0x26, // Fullwidth Ampersand | |
| 472 | 0xff07 => 0x27, // Fullwidth Apostrophe | |
| 473 | 0xff08 => 0x28, // Fullwidth Left Parenthesis | |
| 474 | 0xff09 => 0x29, // Fullwidth Right Parenthesis | |
| 475 | 0xff0a => 0x2a, // Fullwidth Asterisk | |
| 476 | 0xff0b => 0x2b, // Fullwidth Plus Sign | |
| 477 | 0xff0c => 0x2c, // Fullwidth Comma | |
| 478 | 0xff0d => 0x2d, // Fullwidth Hyphen-Minus | |
| 479 | 0xff0e => 0x2e, // Fullwidth Full Stop | |
| 480 | 0xff0f => 0x2f, // Fullwidth Solidus | |
| 481 | 0xff10 => 0x30, // Fullwidth Digit Zero | |
| 482 | 0xff11 => 0x31, // Fullwidth Digit One | |
| 483 | 0xff12 => 0x32, // Fullwidth Digit Two | |
| 484 | 0xff13 => 0x33, // Fullwidth Digit Three | |
| 485 | 0xff14 => 0x34, // Fullwidth Digit Four | |
| 486 | 0xff15 => 0x35, // Fullwidth Digit Five | |
| 487 | 0xff16 => 0x36, // Fullwidth Digit Six | |
| 488 | 0xff17 => 0x37, // Fullwidth Digit Seven | |
| 489 | 0xff18 => 0x38, // Fullwidth Digit Eight | |
| 490 | 0xff19 => 0x39, // Fullwidth Digit Nine | |
| 491 | 0xff1a => 0x3a, // Fullwidth Colon | |
| 492 | 0xff1b => 0x3b, // Fullwidth Semicolon | |
| 493 | 0xff1c => 0x3c, // Fullwidth Less-Than Sign | |
| 494 | 0xff1d => 0x3d, // Fullwidth Equals Sign | |
| 495 | 0xff1e => 0x3e, // Fullwidth Greater-Than Sign | |
| 496 | 0xff1f => 0x3f, // Fullwidth Question Mark | |
| 497 | 0xff20 => 0x40, // Fullwidth Commercial At | |
| 498 | 0xff21 => 0x41, // Fullwidth Latin Capital Letter A | |
| 499 | 0xff22 => 0x42, // Fullwidth Latin Capital Letter B | |
| 500 | 0xff23 => 0x43, // Fullwidth Latin Capital Letter C | |
| 501 | 0xff24 => 0x44, // Fullwidth Latin Capital Letter D | |
| 502 | 0xff25 => 0x45, // Fullwidth Latin Capital Letter E | |
| 503 | 0xff26 => 0x46, // Fullwidth Latin Capital Letter F | |
| 504 | 0xff27 => 0x47, // Fullwidth Latin Capital Letter G | |
| 505 | 0xff28 => 0x48, // Fullwidth Latin Capital Letter H | |
| 506 | 0xff29 => 0x49, // Fullwidth Latin Capital Letter I | |
| 507 | 0xff2a => 0x4a, // Fullwidth Latin Capital Letter J | |
| 508 | 0xff2b => 0x4b, // Fullwidth Latin Capital Letter K | |
| 509 | 0xff2c => 0x4c, // Fullwidth Latin Capital Letter L | |
| 510 | 0xff2d => 0x4d, // Fullwidth Latin Capital Letter M | |
| 511 | 0xff2e => 0x4e, // Fullwidth Latin Capital Letter N | |
| 512 | 0xff2f => 0x4f, // Fullwidth Latin Capital Letter O | |
| 513 | 0xff30 => 0x50, // Fullwidth Latin Capital Letter P | |
| 514 | 0xff31 => 0x51, // Fullwidth Latin Capital Letter Q | |
| 515 | 0xff32 => 0x52, // Fullwidth Latin Capital Letter R | |
| 516 | 0xff33 => 0x53, // Fullwidth Latin Capital Letter S | |
| 517 | 0xff34 => 0x54, // Fullwidth Latin Capital Letter T | |
| 518 | 0xff35 => 0x55, // Fullwidth Latin Capital Letter U | |
| 519 | 0xff36 => 0x56, // Fullwidth Latin Capital Letter V | |
| 520 | 0xff37 => 0x57, // Fullwidth Latin Capital Letter W | |
| 521 | 0xff38 => 0x58, // Fullwidth Latin Capital Letter X | |
| 522 | 0xff39 => 0x59, // Fullwidth Latin Capital Letter Y | |
| 523 | 0xff3a => 0x5a, // Fullwidth Latin Capital Letter Z | |
| 524 | 0xff3b => 0x5b, // Fullwidth Left Square Bracket | |
| 525 | 0xff3c => 0x5c, // Fullwidth Reverse Solidus | |
| 526 | 0xff3d => 0x5d, // Fullwidth Right Square Bracket | |
| 527 | 0xff3e => 0x5e, // Fullwidth Circumflex Accent | |
| 528 | 0xff3f => 0x5f, // Fullwidth Low Line | |
| 529 | 0xff40 => 0x60, // Fullwidth Grave Accent | |
| 530 | 0xff41 => 0x61, // Fullwidth Latin Small Letter A | |
| 531 | 0xff42 => 0x62, // Fullwidth Latin Small Letter B | |
| 532 | 0xff43 => 0x63, // Fullwidth Latin Small Letter C | |
| 533 | 0xff44 => 0x64, // Fullwidth Latin Small Letter D | |
| 534 | 0xff45 => 0x65, // Fullwidth Latin Small Letter E | |
| 535 | 0xff46 => 0x66, // Fullwidth Latin Small Letter F | |
| 536 | 0xff47 => 0x67, // Fullwidth Latin Small Letter G | |
| 537 | 0xff48 => 0x68, // Fullwidth Latin Small Letter H | |
| 538 | 0xff49 => 0x69, // Fullwidth Latin Small Letter I | |
| 539 | 0xff4a => 0x6a, // Fullwidth Latin Small Letter J | |
| 540 | 0xff4b => 0x6b, // Fullwidth Latin Small Letter K | |
| 541 | 0xff4c => 0x6c, // Fullwidth Latin Small Letter L | |
| 542 | 0xff4d => 0x6d, // Fullwidth Latin Small Letter M | |
| 543 | 0xff4e => 0x6e, // Fullwidth Latin Small Letter N | |
| 544 | 0xff4f => 0x6f, // Fullwidth Latin Small Letter O | |
| 545 | 0xff50 => 0x70, // Fullwidth Latin Small Letter P | |
| 546 | 0xff51 => 0x71, // Fullwidth Latin Small Letter Q | |
| 547 | 0xff52 => 0x72, // Fullwidth Latin Small Letter R | |
| 548 | 0xff53 => 0x73, // Fullwidth Latin Small Letter S | |
| 549 | 0xff54 => 0x74, // Fullwidth Latin Small Letter T | |
| 550 | 0xff55 => 0x75, // Fullwidth Latin Small Letter U | |
| 551 | 0xff56 => 0x76, // Fullwidth Latin Small Letter V | |
| 552 | 0xff57 => 0x77, // Fullwidth Latin Small Letter W | |
| 553 | 0xff58 => 0x78, // Fullwidth Latin Small Letter X | |
| 554 | 0xff59 => 0x79, // Fullwidth Latin Small Letter Y | |
| 555 | 0xff5a => 0x7a, // Fullwidth Latin Small Letter Z | |
| 556 | 0xff5b => 0x7b, // Fullwidth Left Curly Bracket | |
| 557 | 0xff5c => 0x7c, // Fullwidth Vertical Line | |
| 558 | 0xff5d => 0x7d, // Fullwidth Right Curly Bracket | |
| 559 | 0xff5e => 0x7e, // Fullwidth Tilde | |
| 560 | // Not in the best fit mapping, but RC uses these mappings too | |
| 561 | 0x2007 => 0xA0, // Figure Space | |
| 562 | 0x2008 => ' ', // Punctuation Space | |
| 563 | 0x2009 => ' ', // Thin Space | |
| 564 | 0x200A => ' ', // Hair Space | |
| 565 | 0x2012 => '-', // Figure Dash | |
| 566 | 0x2015 => '-', // Horizontal Bar | |
| 567 | 0x201B => '\'', // Single High-reversed-9 Quotation Mark | |
| 568 | 0x201F => '"', // Double High-reversed-9 Quotation Mark | |
| 569 | 0x202F => 0xA0, // Narrow No-Break Space | |
| 570 | 0x2033 => '"', // Double Prime | |
| 571 | 0x2036 => '"', // Reversed Double Prime | |
| 572 | else => null, | |
| 573 | }; | |
| 574 | } | |
| 575 | ||
| 576 | test "windows-1252 to utf8" { | |
| 577 | var buf = std.ArrayList(u8).init(std.testing.allocator); | |
| 578 | defer buf.deinit(); | |
| 579 | ||
| 580 | const input_windows1252 = "\x81pqrstuvwxyz{|}~\x80\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8e\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9e\x9f\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"; | |
| 581 | const expected_utf8 = "\xc2\x81pqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"; | |
| 582 | ||
| 583 | var fbs = std.io.fixedBufferStream(input_windows1252); | |
| 584 | const bytes_written = try windows1252ToUtf8Stream(buf.writer(), fbs.reader()); | |
| 585 | ||
| 586 | try std.testing.expectEqualStrings(expected_utf8, buf.items); | |
| 587 | try std.testing.expectEqual(expected_utf8.len, bytes_written); | |
| 588 | } |