| author | |
| committer | |
| log | 52de2802c457140f3d9923cf014b51bb8c16689f |
| tree | ed2accf0d3b85155e01ea7063792f28ec3d7184b |
| parent | d0c06ca7127110a8afeb0ef524a197049892db21 |
This moves .rc/.manifest compilation out of the main Zig binary, contributing towards #19063
Also:
- Make resinator use Aro as its preprocessor instead of clang
- Sync resinator with upstream42 files changed, 17002 insertions(+), 16939 deletions(-)
lib/compiler/resinator/ani.zig created+58| ... | @@ -0,0 +1,58 @@ | ||
| 1 | //! https://en.wikipedia.org/wiki/Resource_Interchange_File_Format | ||
| 2 | //! https://www.moon-soft.com/program/format/windows/ani.htm | ||
| 3 | //! https://www.gdgsoft.com/anituner/help/aniformat.htm | ||
| 4 | //! https://www.lomont.org/software/aniexploit/ExploitANI.pdf | ||
| 5 | //! | ||
| 6 | //! RIFF( 'ACON' | ||
| 7 | //! [LIST( 'INFO' <info_data> )] | ||
| 8 | //! [<DISP_ck>] | ||
| 9 | //! anih( <ani_header> ) | ||
| 10 | //! [rate( <rate_info> )] | ||
| 11 | //! ['seq '( <sequence_info> )] | ||
| 12 | //! LIST( 'fram' icon( <icon_file> ) ... ) | ||
| 13 | //! ) | ||
| 14 | |||
| 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+298| ... | @@ -0,0 +1,298 @@ | ||
| 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 | const stderr = std.io.getStdErr(); | ||
| 18 | const stderr_config = std.io.tty.detectConfig(stderr); | ||
| 19 | |||
| 20 | const args = try std.process.argsAlloc(allocator); | ||
| 21 | defer std.process.argsFree(allocator, args); | ||
| 22 | |||
| 23 | if (args.len < 2) { | ||
| 24 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "expected zig lib dir as first argument", .{}); | ||
| 25 | std.os.exit(1); | ||
| 26 | } | ||
| 27 | const zig_lib_dir = args[1]; | ||
| 28 | |||
| 29 | var options = options: { | ||
| 30 | var cli_diagnostics = cli.Diagnostics.init(allocator); | ||
| 31 | defer cli_diagnostics.deinit(); | ||
| 32 | var options = cli.parse(allocator, args[2..], &cli_diagnostics) catch |err| switch (err) { | ||
| 33 | error.ParseError => { | ||
| 34 | cli_diagnostics.renderToStdErr(args, stderr_config); | ||
| 35 | std.os.exit(1); | ||
| 36 | }, | ||
| 37 | else => |e| return e, | ||
| 38 | }; | ||
| 39 | try options.maybeAppendRC(std.fs.cwd()); | ||
| 40 | |||
| 41 | // print any warnings/notes | ||
| 42 | cli_diagnostics.renderToStdErr(args, stderr_config); | ||
| 43 | // If there was something printed, then add an extra newline separator | ||
| 44 | // so that there is a clear separation between the cli diagnostics and whatever | ||
| 45 | // gets printed after | ||
| 46 | if (cli_diagnostics.errors.items.len > 0) { | ||
| 47 | try stderr.writeAll("\n"); | ||
| 48 | } | ||
| 49 | break :options options; | ||
| 50 | }; | ||
| 51 | defer options.deinit(); | ||
| 52 | |||
| 53 | if (options.print_help_and_exit) { | ||
| 54 | try cli.writeUsage(stderr.writer(), "zig rc"); | ||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | const stdout_writer = std.io.getStdOut().writer(); | ||
| 59 | if (options.verbose) { | ||
| 60 | try options.dumpVerbose(stdout_writer); | ||
| 61 | try stdout_writer.writeByte('\n'); | ||
| 62 | } | ||
| 63 | |||
| 64 | var dependencies_list = std.ArrayList([]const u8).init(allocator); | ||
| 65 | defer { | ||
| 66 | for (dependencies_list.items) |item| { | ||
| 67 | allocator.free(item); | ||
| 68 | } | ||
| 69 | dependencies_list.deinit(); | ||
| 70 | } | ||
| 71 | const maybe_dependencies_list: ?*std.ArrayList([]const u8) = if (options.depfile_path != null) &dependencies_list else null; | ||
| 72 | |||
| 73 | const full_input = full_input: { | ||
| 74 | if (options.preprocess != .no) { | ||
| 75 | var preprocessed_buf = std.ArrayList(u8).init(allocator); | ||
| 76 | errdefer preprocessed_buf.deinit(); | ||
| 77 | |||
| 78 | // We're going to throw away everything except the final preprocessed output anyway, | ||
| 79 | // so we can use a scoped arena for everything else. | ||
| 80 | var aro_arena_state = std.heap.ArenaAllocator.init(allocator); | ||
| 81 | defer aro_arena_state.deinit(); | ||
| 82 | const aro_arena = aro_arena_state.allocator(); | ||
| 83 | |||
| 84 | const include_paths = getIncludePaths(aro_arena, options.auto_includes, zig_lib_dir) catch |err| switch (err) { | ||
| 85 | error.OutOfMemory => |e| return e, | ||
| 86 | else => |e| { | ||
| 87 | switch (e) { | ||
| 88 | error.MsvcIncludesNotFound => { | ||
| 89 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "MSVC include paths could not be automatically detected", .{}); | ||
| 90 | }, | ||
| 91 | error.MingwIncludesNotFound => { | ||
| 92 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "MinGW include paths could not be automatically detected", .{}); | ||
| 93 | }, | ||
| 94 | } | ||
| 95 | try renderErrorMessage(stderr.writer(), stderr_config, .note, "to disable auto includes, use the option /:auto-includes none", .{}); | ||
| 96 | std.os.exit(1); | ||
| 97 | }, | ||
| 98 | }; | ||
| 99 | |||
| 100 | var comp = aro.Compilation.init(aro_arena); | ||
| 101 | defer comp.deinit(); | ||
| 102 | |||
| 103 | var argv = std.ArrayList([]const u8).init(comp.gpa); | ||
| 104 | defer argv.deinit(); | ||
| 105 | |||
| 106 | try argv.append("arocc"); // dummy command name | ||
| 107 | try preprocess.appendAroArgs(aro_arena, &argv, options, include_paths); | ||
| 108 | try argv.append(options.input_filename); | ||
| 109 | |||
| 110 | if (options.verbose) { | ||
| 111 | try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n"); | ||
| 112 | for (argv.items[0 .. argv.items.len - 1]) |arg| { | ||
| 113 | try stdout_writer.print("{s} ", .{arg}); | ||
| 114 | } | ||
| 115 | try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]}); | ||
| 116 | } | ||
| 117 | |||
| 118 | preprocess.preprocess(&comp, preprocessed_buf.writer(), argv.items, maybe_dependencies_list) catch |err| switch (err) { | ||
| 119 | error.GeneratedSourceError => { | ||
| 120 | // extra newline to separate this line from the aro errors | ||
| 121 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessor setup (this is always a bug):\n", .{}); | ||
| 122 | aro.Diagnostics.render(&comp, stderr_config); | ||
| 123 | std.os.exit(1); | ||
| 124 | }, | ||
| 125 | // ArgError can occur if e.g. the .rc file is not found | ||
| 126 | error.ArgError, error.PreprocessError => { | ||
| 127 | // extra newline to separate this line from the aro errors | ||
| 128 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing:\n", .{}); | ||
| 129 | aro.Diagnostics.render(&comp, stderr_config); | ||
| 130 | std.os.exit(1); | ||
| 131 | }, | ||
| 132 | error.StreamTooLong => { | ||
| 133 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing: maximum file size exceeded", .{}); | ||
| 134 | std.os.exit(1); | ||
| 135 | }, | ||
| 136 | error.OutOfMemory => |e| return e, | ||
| 137 | }; | ||
| 138 | |||
| 139 | break :full_input try preprocessed_buf.toOwnedSlice(); | ||
| 140 | } else { | ||
| 141 | break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| { | ||
| 142 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) }); | ||
| 143 | std.os.exit(1); | ||
| 144 | }; | ||
| 145 | } | ||
| 146 | }; | ||
| 147 | defer allocator.free(full_input); | ||
| 148 | |||
| 149 | if (options.preprocess == .only) { | ||
| 150 | try std.fs.cwd().writeFile(options.output_filename, full_input); | ||
| 151 | return; | ||
| 152 | } | ||
| 153 | |||
| 154 | // Note: We still want to run this when no-preprocess is set because: | ||
| 155 | // 1. We want to print accurate line numbers after removing multiline comments | ||
| 156 | // 2. We want to be able to handle an already-preprocessed input with #line commands in it | ||
| 157 | var mapping_results = try parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }); | ||
| 158 | defer mapping_results.mappings.deinit(allocator); | ||
| 159 | |||
| 160 | const final_input = removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings) catch |err| switch (err) { | ||
| 161 | error.InvalidSourceMappingCollapse => { | ||
| 162 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during comment removal; this is a known bug", .{}); | ||
| 163 | std.os.exit(1); | ||
| 164 | }, | ||
| 165 | else => |e| return e, | ||
| 166 | }; | ||
| 167 | |||
| 168 | var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| { | ||
| 169 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) }); | ||
| 170 | std.os.exit(1); | ||
| 171 | }; | ||
| 172 | var output_file_closed = false; | ||
| 173 | defer if (!output_file_closed) output_file.close(); | ||
| 174 | |||
| 175 | var diagnostics = Diagnostics.init(allocator); | ||
| 176 | defer diagnostics.deinit(); | ||
| 177 | |||
| 178 | var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); | ||
| 179 | |||
| 180 | compile(allocator, final_input, output_buffered_stream.writer(), .{ | ||
| 181 | .cwd = std.fs.cwd(), | ||
| 182 | .diagnostics = &diagnostics, | ||
| 183 | .source_mappings = &mapping_results.mappings, | ||
| 184 | .dependencies_list = maybe_dependencies_list, | ||
| 185 | .ignore_include_env_var = options.ignore_include_env_var, | ||
| 186 | .extra_include_paths = options.extra_include_paths.items, | ||
| 187 | .default_language_id = options.default_language_id, | ||
| 188 | .default_code_page = options.default_code_page orelse .windows1252, | ||
| 189 | .verbose = options.verbose, | ||
| 190 | .null_terminate_string_table_strings = options.null_terminate_string_table_strings, | ||
| 191 | .max_string_literal_codepoints = options.max_string_literal_codepoints, | ||
| 192 | .silent_duplicate_control_ids = options.silent_duplicate_control_ids, | ||
| 193 | .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, | ||
| 194 | }) catch |err| switch (err) { | ||
| 195 | error.ParseError, error.CompileError => { | ||
| 196 | diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); | ||
| 197 | // Delete the output file on error | ||
| 198 | output_file.close(); | ||
| 199 | output_file_closed = true; | ||
| 200 | // Failing to delete is not really a big deal, so swallow any errors | ||
| 201 | std.fs.cwd().deleteFile(options.output_filename) catch {}; | ||
| 202 | std.os.exit(1); | ||
| 203 | }, | ||
| 204 | else => |e| return e, | ||
| 205 | }; | ||
| 206 | |||
| 207 | try output_buffered_stream.flush(); | ||
| 208 | |||
| 209 | // print any warnings/notes | ||
| 210 | diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); | ||
| 211 | |||
| 212 | // write the depfile | ||
| 213 | if (options.depfile_path) |depfile_path| { | ||
| 214 | var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { | ||
| 215 | try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); | ||
| 216 | std.os.exit(1); | ||
| 217 | }; | ||
| 218 | defer depfile.close(); | ||
| 219 | |||
| 220 | const depfile_writer = depfile.writer(); | ||
| 221 | var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer); | ||
| 222 | switch (options.depfile_fmt) { | ||
| 223 | .json => { | ||
| 224 | var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 }); | ||
| 225 | defer write_stream.deinit(); | ||
| 226 | |||
| 227 | try write_stream.beginArray(); | ||
| 228 | for (dependencies_list.items) |dep_path| { | ||
| 229 | try write_stream.write(dep_path); | ||
| 230 | } | ||
| 231 | try write_stream.endArray(); | ||
| 232 | }, | ||
| 233 | } | ||
| 234 | try depfile_buffered_writer.flush(); | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 { | ||
| 239 | var includes = auto_includes_option; | ||
| 240 | if (builtin.target.os.tag != .windows) { | ||
| 241 | switch (includes) { | ||
| 242 | // MSVC can't be found when the host isn't Windows, so short-circuit. | ||
| 243 | .msvc => return error.MsvcIncludesNotFound, | ||
| 244 | // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts. | ||
| 245 | .any => includes = .gnu, | ||
| 246 | .none, .gnu => {}, | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | while (true) { | ||
| 251 | switch (includes) { | ||
| 252 | .none => return &[_][]const u8{}, | ||
| 253 | .any, .msvc => { | ||
| 254 | // MSVC is only detectable on Windows targets. This unreachable is to signify | ||
| 255 | // that .any and .msvc should be dealt with on non-Windows targets before this point, | ||
| 256 | // since getting MSVC include paths uses Windows-only APIs. | ||
| 257 | if (builtin.target.os.tag != .windows) unreachable; | ||
| 258 | |||
| 259 | const target_query: std.Target.Query = .{ | ||
| 260 | .os_tag = .windows, | ||
| 261 | .abi = .msvc, | ||
| 262 | }; | ||
| 263 | const target = std.zig.resolveTargetQueryOrFatal(target_query); | ||
| 264 | const is_native_abi = target_query.isNativeAbi(); | ||
| 265 | const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch { | ||
| 266 | if (includes == .any) { | ||
| 267 | // fall back to mingw | ||
| 268 | includes = .gnu; | ||
| 269 | continue; | ||
| 270 | } | ||
| 271 | return error.MsvcIncludesNotFound; | ||
| 272 | }; | ||
| 273 | if (detected_libc.libc_include_dir_list.len == 0) { | ||
| 274 | if (includes == .any) { | ||
| 275 | // fall back to mingw | ||
| 276 | includes = .gnu; | ||
| 277 | continue; | ||
| 278 | } | ||
| 279 | return error.MsvcIncludesNotFound; | ||
| 280 | } | ||
| 281 | return detected_libc.libc_include_dir_list; | ||
| 282 | }, | ||
| 283 | .gnu => { | ||
| 284 | const target_query: std.Target.Query = .{ | ||
| 285 | .os_tag = .windows, | ||
| 286 | .abi = .gnu, | ||
| 287 | }; | ||
| 288 | const target = std.zig.resolveTargetQueryOrFatal(target_query); | ||
| 289 | const is_native_abi = target_query.isNativeAbi(); | ||
| 290 | const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| switch (err) { | ||
| 291 | error.OutOfMemory => |e| return e, | ||
| 292 | else => return error.MingwIncludesNotFound, | ||
| 293 | }; | ||
| 294 | return detected_libc.libc_include_dir_list; | ||
| 295 | }, | ||
| 296 | } | ||
| 297 | } | ||
| 298 | } | ||
lib/compiler/resinator/parse.zig created+1897| ... | @@ -0,0 +1,1897 @@ | ||
| 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+122| ... | @@ -0,0 +1,122 @@ | ||
| 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 | } | ||
| 113 | |||
| 114 | pub fn isLineEndingPair(first: u8, second: u8) bool { | ||
| 115 | if (first != '\r' and first != '\n') return false; | ||
| 116 | if (second != '\r' and second != '\n') return false; | ||
| 117 | |||
| 118 | // can't be \n\n or \r\r | ||
| 119 | if (first == second) return false; | ||
| 120 | |||
| 121 | return true; | ||
| 122 | } | ||
lib/compiler/resinator/windows1252.zig created+588| ... | @@ -0,0 +1,588 @@ | ||
| 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 | } | ||
src/Compilation.zig+121-445| ... | @@ -36,7 +36,6 @@ const Cache = std.Build.Cache; | ... | @@ -36,7 +36,6 @@ const Cache = std.Build.Cache; |
| 36 | const c_codegen = @import("codegen/c.zig"); | 36 | const c_codegen = @import("codegen/c.zig"); |
| 37 | const libtsan = @import("libtsan.zig"); | 37 | const libtsan = @import("libtsan.zig"); |
| 38 | const Zir = std.zig.Zir; | 38 | const Zir = std.zig.Zir; |
| 39 | const resinator = @import("resinator.zig"); | ||
| 40 | const Builtin = @import("Builtin.zig"); | 39 | const Builtin = @import("Builtin.zig"); |
| 41 | const LlvmObject = @import("codegen/llvm.zig").Object; | 40 | const LlvmObject = @import("codegen/llvm.zig").Object; |
| 42 | 41 | ||
| ... | @@ -174,7 +173,7 @@ local_cache_directory: Directory, | ... | @@ -174,7 +173,7 @@ local_cache_directory: Directory, |
| 174 | global_cache_directory: Directory, | 173 | global_cache_directory: Directory, |
| 175 | libc_include_dir_list: []const []const u8, | 174 | libc_include_dir_list: []const []const u8, |
| 176 | libc_framework_dir_list: []const []const u8, | 175 | libc_framework_dir_list: []const []const u8, |
| 177 | rc_include_dir_list: []const []const u8, | 176 | rc_includes: RcIncludes, |
| 178 | thread_pool: *ThreadPool, | 177 | thread_pool: *ThreadPool, |
| 179 | 178 | ||
| 180 | /// Populated when we build the libc++ static library. A Job to build this is placed in the queue | 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,68 +1242,6 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil |
| 1243 | options.libc_installation, | 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 | const sysroot = options.sysroot orelse libc_dirs.sysroot; | 1245 | const sysroot = options.sysroot orelse libc_dirs.sysroot; |
| 1309 | 1246 | ||
| 1310 | const include_compiler_rt = options.want_compiler_rt orelse | 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,7 +1429,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil |
| 1492 | .self_exe_path = options.self_exe_path, | 1429 | .self_exe_path = options.self_exe_path, |
| 1493 | .libc_include_dir_list = libc_dirs.libc_include_dir_list, | 1430 | .libc_include_dir_list = libc_dirs.libc_include_dir_list, |
| 1494 | .libc_framework_dir_list = libc_dirs.libc_framework_dir_list, | 1431 | .libc_framework_dir_list = libc_dirs.libc_framework_dir_list, |
| 1495 | .rc_include_dir_list = rc_dirs.libc_include_dir_list, | 1432 | .rc_includes = options.rc_includes, |
| 1496 | .thread_pool = options.thread_pool, | 1433 | .thread_pool = options.thread_pool, |
| 1497 | .clang_passthrough_mode = options.clang_passthrough_mode, | 1434 | .clang_passthrough_mode = options.clang_passthrough_mode, |
| 1498 | .clang_preprocessor_mode = options.clang_preprocessor_mode, | 1435 | .clang_preprocessor_mode = options.clang_preprocessor_mode, |
| ... | @@ -2506,7 +2443,7 @@ fn addNonIncrementalStuffToCacheManifest( | ... | @@ -2506,7 +2443,7 @@ fn addNonIncrementalStuffToCacheManifest( |
| 2506 | man.hash.add(comp.link_eh_frame_hdr); | 2443 | man.hash.add(comp.link_eh_frame_hdr); |
| 2507 | man.hash.add(comp.skip_linker_dependencies); | 2444 | man.hash.add(comp.skip_linker_dependencies); |
| 2508 | man.hash.add(comp.include_compiler_rt); | 2445 | man.hash.add(comp.include_compiler_rt); |
| 2509 | man.hash.addListOfBytes(comp.rc_include_dir_list); | 2446 | man.hash.add(comp.rc_includes); |
| 2510 | man.hash.addListOfBytes(comp.force_undefined_symbols.keys()); | 2447 | man.hash.addListOfBytes(comp.force_undefined_symbols.keys()); |
| 2511 | man.hash.addListOfBytes(comp.framework_dirs); | 2448 | man.hash.addListOfBytes(comp.framework_dirs); |
| 2512 | try link.hashAddSystemLibs(man, comp.system_libs); | 2449 | try link.hashAddSystemLibs(man, comp.system_libs); |
| ... | @@ -4172,7 +4109,7 @@ pub fn obtainCObjectCacheManifest( | ... | @@ -4172,7 +4109,7 @@ pub fn obtainCObjectCacheManifest( |
| 4172 | pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest { | 4109 | pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest { |
| 4173 | var man = comp.cache_parent.obtain(); | 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 | return man; | 4114 | return man; |
| 4178 | } | 4115 | } |
| ... | @@ -4812,11 +4749,12 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P | ... | @@ -4812,11 +4749,12 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P |
| 4812 | } | 4749 | } |
| 4813 | 4750 | ||
| 4814 | fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void { | 4751 | fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void { |
| 4815 | if (!build_options.have_llvm) { | 4752 | if (!std.process.can_spawn) { |
| 4816 | return comp.failWin32Resource(win32_resource, "clang not available: compiler built without LLVM extensions", .{}); | 4753 | return comp.failWin32Resource(win32_resource, "{s} does not support spawning a child process", .{@tagName(builtin.os.tag)}); |
| 4817 | } | 4754 | } |
| 4755 | |||
| 4818 | const self_exe_path = comp.self_exe_path orelse | 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 | const tracy_trace = trace(@src()); | 4759 | const tracy_trace = trace(@src()); |
| 4822 | defer tracy_trace.end(); | 4760 | defer tracy_trace.end(); |
| ... | @@ -4856,6 +4794,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 | ... | @@ -4856,6 +4794,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4856 | if (win32_resource.src == .manifest) { | 4794 | if (win32_resource.src == .manifest) { |
| 4857 | _ = try man.addFile(src_path, null); | 4795 | _ = try man.addFile(src_path, null); |
| 4858 | 4796 | ||
| 4797 | const rc_basename = try std.fmt.allocPrint(arena, "{s}.rc", .{src_basename}); | ||
| 4859 | const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{src_basename}); | 4798 | const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{src_basename}); |
| 4860 | 4799 | ||
| 4861 | const digest = if (try man.hit()) man.final() else blk: { | 4800 | const digest = if (try man.hit()) man.final() else blk: { |
| ... | @@ -4867,17 +4806,12 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 | ... | @@ -4867,17 +4806,12 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4867 | var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{}); | 4806 | var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{}); |
| 4868 | defer o_dir.close(); | 4807 | defer o_dir.close(); |
| 4869 | 4808 | ||
| 4870 | var output_file = o_dir.createFile(res_basename, .{}) catch |err| { | 4809 | const in_rc_path = try comp.local_cache_directory.join(comp.gpa, &.{ |
| 4871 | const output_file_path = try comp.local_cache_directory.join(arena, &.{ o_sub_path, res_basename }); | 4810 | o_sub_path, rc_basename, |
| 4872 | return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ output_file_path, @errorName(err) }); | 4811 | }); |
| 4873 | }; | 4812 | const out_res_path = try comp.local_cache_directory.join(comp.gpa, &.{ |
| 4874 | var output_file_closed = false; | 4813 | o_sub_path, res_basename, |
| 4875 | defer if (!output_file_closed) output_file.close(); | 4814 | }); |
| 4876 | |||
| 4877 | var diagnostics = resinator.errors.Diagnostics.init(arena); | ||
| 4878 | defer diagnostics.deinit(); | ||
| 4879 | |||
| 4880 | var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); | ||
| 4881 | 4815 | ||
| 4882 | // In .rc files, a " within a quoted string is escaped as "" | 4816 | // In .rc files, a " within a quoted string is escaped as "" |
| 4883 | const fmtRcEscape = struct { | 4817 | const fmtRcEscape = struct { |
| ... | @@ -4899,28 +4833,47 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 | ... | @@ -4899,28 +4833,47 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4899 | // 1 is CREATEPROCESS_MANIFEST_RESOURCE_ID which is the default ID used for RT_MANIFEST resources | 4833 | // 1 is CREATEPROCESS_MANIFEST_RESOURCE_ID which is the default ID used for RT_MANIFEST resources |
| 4900 | // 24 is RT_MANIFEST | 4834 | // 24 is RT_MANIFEST |
| 4901 | const input = try std.fmt.allocPrint(arena, "1 24 \"{s}\"", .{fmtRcEscape(src_path)}); | 4835 | const input = try std.fmt.allocPrint(arena, "1 24 \"{s}\"", .{fmtRcEscape(src_path)}); |
| 4836 | try o_dir.writeFile(rc_basename, input); | ||
| 4837 | |||
| 4838 | var argv = std.ArrayList([]const u8).init(comp.gpa); | ||
| 4839 | defer argv.deinit(); | ||
| 4840 | |||
| 4841 | try argv.appendSlice(&.{ | ||
| 4842 | self_exe_path, | ||
| 4843 | "rc", | ||
| 4844 | "/:no-preprocess", | ||
| 4845 | "/x", // ignore INCLUDE environment variable | ||
| 4846 | "/c65001", // UTF-8 codepage | ||
| 4847 | "/:auto-includes", | ||
| 4848 | "none", | ||
| 4849 | }); | ||
| 4850 | try argv.appendSlice(&.{ "--", in_rc_path, out_res_path }); | ||
| 4902 | 4851 | ||
| 4903 | resinator.compile.compile(arena, input, output_buffered_stream.writer(), .{ | 4852 | var child = std.ChildProcess.init(argv.items, arena); |
| 4904 | .cwd = std.fs.cwd(), | 4853 | child.stdin_behavior = .Ignore; |
| 4905 | .diagnostics = &diagnostics, | 4854 | child.stdout_behavior = .Ignore; |
| 4906 | .ignore_include_env_var = true, | 4855 | child.stderr_behavior = .Pipe; |
| 4907 | .default_code_page = .utf8, | 4856 | |
| 4908 | }) catch |err| switch (err) { | 4857 | try child.spawn(); |
| 4909 | error.ParseError, error.CompileError => { | 4858 | |
| 4910 | // Delete the output file on error | 4859 | const stderr_reader = child.stderr.?.reader(); |
| 4911 | output_file.close(); | 4860 | const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024); |
| 4912 | output_file_closed = true; | 4861 | const term = child.wait() catch |err| { |
| 4913 | // Failing to delete is not really a big deal, so swallow any errors | 4862 | return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); |
| 4914 | o_dir.deleteFile(res_basename) catch { | ||
| 4915 | const output_file_path = try comp.local_cache_directory.join(arena, &.{ o_sub_path, res_basename }); | ||
| 4916 | log.warn("failed to delete '{s}': {s}", .{ output_file_path, @errorName(err) }); | ||
| 4917 | }; | ||
| 4918 | return comp.failWin32ResourceCompile(win32_resource, input, &diagnostics, null); | ||
| 4919 | }, | ||
| 4920 | else => |e| return e, | ||
| 4921 | }; | 4863 | }; |
| 4922 | 4864 | ||
| 4923 | try output_buffered_stream.flush(); | 4865 | switch (term) { |
| 4866 | .Exited => |code| { | ||
| 4867 | if (code != 0) { | ||
| 4868 | log.err("zig rc failed with stderr:\n{s}", .{stderr}); | ||
| 4869 | return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code}); | ||
| 4870 | } | ||
| 4871 | }, | ||
| 4872 | else => { | ||
| 4873 | log.err("zig rc terminated with stderr:\n{s}", .{stderr}); | ||
| 4874 | return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); | ||
| 4875 | }, | ||
| 4876 | } | ||
| 4924 | 4877 | ||
| 4925 | break :blk digest; | 4878 | break :blk digest; |
| 4926 | }; | 4879 | }; |
| ... | @@ -4951,9 +4904,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 | ... | @@ -4951,9 +4904,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4951 | const rc_basename_noext = src_basename[0 .. src_basename.len - std.fs.path.extension(src_basename).len]; | 4904 | const rc_basename_noext = src_basename[0 .. src_basename.len - std.fs.path.extension(src_basename).len]; |
| 4952 | 4905 | ||
| 4953 | const digest = if (try man.hit()) man.final() else blk: { | 4906 | const digest = if (try man.hit()) man.final() else blk: { |
| 4954 | const rcpp_filename = try std.fmt.allocPrint(arena, "{s}.rcpp", .{rc_basename_noext}); | ||
| 4955 | |||
| 4956 | const out_rcpp_path = try comp.tmpFilePath(arena, rcpp_filename); | ||
| 4957 | var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{}); | 4907 | var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{}); |
| 4958 | defer zig_cache_tmp_dir.close(); | 4908 | defer zig_cache_tmp_dir.close(); |
| 4959 | 4909 | ||
| ... | @@ -4963,193 +4913,89 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 | ... | @@ -4963,193 +4913,89 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 4963 | // so we need a temporary filename. | 4913 | // so we need a temporary filename. |
| 4964 | const out_res_path = try comp.tmpFilePath(arena, res_filename); | 4914 | const out_res_path = try comp.tmpFilePath(arena, res_filename); |
| 4965 | 4915 | ||
| 4966 | var options = options: { | ||
| 4967 | var resinator_args = try std.ArrayListUnmanaged([]const u8).initCapacity(comp.gpa, rc_src.extra_flags.len + 4); | ||
| 4968 | defer resinator_args.deinit(comp.gpa); | ||
| 4969 | |||
| 4970 | resinator_args.appendAssumeCapacity(""); // dummy 'process name' arg | ||
| 4971 | resinator_args.appendSliceAssumeCapacity(rc_src.extra_flags); | ||
| 4972 | resinator_args.appendSliceAssumeCapacity(&.{ "--", out_rcpp_path, out_res_path }); | ||
| 4973 | |||
| 4974 | var cli_diagnostics = resinator.cli.Diagnostics.init(comp.gpa); | ||
| 4975 | defer cli_diagnostics.deinit(); | ||
| 4976 | const options = resinator.cli.parse(comp.gpa, resinator_args.items, &cli_diagnostics) catch |err| switch (err) { | ||
| 4977 | error.ParseError => { | ||
| 4978 | return comp.failWin32ResourceCli(win32_resource, &cli_diagnostics); | ||
| 4979 | }, | ||
| 4980 | else => |e| return e, | ||
| 4981 | }; | ||
| 4982 | break :options options; | ||
| 4983 | }; | ||
| 4984 | defer options.deinit(); | ||
| 4985 | |||
| 4986 | // We never want to read the INCLUDE environment variable, so | ||
| 4987 | // unconditionally set `ignore_include_env_var` to true | ||
| 4988 | options.ignore_include_env_var = true; | ||
| 4989 | |||
| 4990 | if (options.preprocess != .yes) { | ||
| 4991 | return comp.failWin32Resource(win32_resource, "the '{s}' option is not supported in this context", .{switch (options.preprocess) { | ||
| 4992 | .no => "/:no-preprocess", | ||
| 4993 | .only => "/p", | ||
| 4994 | .yes => unreachable, | ||
| 4995 | }}); | ||
| 4996 | } | ||
| 4997 | |||
| 4998 | var argv = std.ArrayList([]const u8).init(comp.gpa); | 4916 | var argv = std.ArrayList([]const u8).init(comp.gpa); |
| 4999 | defer argv.deinit(); | 4917 | defer argv.deinit(); |
| 5000 | 4918 | ||
| 5001 | try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" }); | 4919 | const depfile_filename = try std.fmt.allocPrint(arena, "{s}.d.json", .{rc_basename_noext}); |
| 5002 | 4920 | const out_dep_path = try comp.tmpFilePath(arena, depfile_filename); | |
| 5003 | try resinator.preprocess.appendClangArgs(arena, &argv, options, .{ | 4921 | try argv.appendSlice(&.{ |
| 5004 | .clang_target = null, // handled by addCCArgs | 4922 | self_exe_path, |
| 5005 | .system_include_paths = &.{}, // handled by addCCArgs | 4923 | "rc", |
| 5006 | .needs_gnu_workaround = comp.getTarget().isGnu(), | 4924 | "/:depfile", |
| 5007 | .nostdinc = false, // handled by addCCArgs | 4925 | out_dep_path, |
| 4926 | "/:depfile-fmt", | ||
| 4927 | "json", | ||
| 4928 | "/x", // ignore INCLUDE environment variable | ||
| 4929 | "/:auto-includes", | ||
| 4930 | @tagName(comp.rc_includes), | ||
| 5008 | }); | 4931 | }); |
| 5009 | 4932 | // While these defines are not normally present when calling rc.exe directly, | |
| 5010 | try argv.append(rc_src.src_path); | ||
| 5011 | try argv.appendSlice(&[_][]const u8{ | ||
| 5012 | "-o", | ||
| 5013 | out_rcpp_path, | ||
| 5014 | }); | ||
| 5015 | |||
| 5016 | const out_dep_path = try std.fmt.allocPrint(arena, "{s}.d", .{out_rcpp_path}); | ||
| 5017 | // Note: addCCArgs will implicitly add _DEBUG/NDEBUG depending on the optimization | ||
| 5018 | // mode. While these defines are not normally present when calling rc.exe directly, | ||
| 5019 | // them being defined matches the behavior of how MSVC calls rc.exe which is the more | 4933 | // them being defined matches the behavior of how MSVC calls rc.exe which is the more |
| 5020 | // relevant behavior in this case. | 4934 | // relevant behavior in this case. |
| 5021 | try comp.addCCArgs(arena, &argv, .rc, out_dep_path, rc_src.owner); | 4935 | switch (rc_src.owner.optimize_mode) { |
| 5022 | 4936 | .Debug => try argv.append("-D_DEBUG"), | |
| 5023 | if (comp.verbose_cc) { | 4937 | .ReleaseSafe => {}, |
| 5024 | dump_argv(argv.items); | 4938 | .ReleaseFast, .ReleaseSmall => try argv.append("-DNDEBUG"), |
| 5025 | } | 4939 | } |
| 4940 | try argv.appendSlice(rc_src.extra_flags); | ||
| 4941 | try argv.appendSlice(&.{ "--", rc_src.src_path, out_res_path }); | ||
| 5026 | 4942 | ||
| 5027 | if (std.process.can_spawn) { | 4943 | var child = std.ChildProcess.init(argv.items, arena); |
| 5028 | var child = std.ChildProcess.init(argv.items, arena); | 4944 | child.stdin_behavior = .Ignore; |
| 5029 | child.stdin_behavior = .Ignore; | 4945 | child.stdout_behavior = .Ignore; |
| 5030 | child.stdout_behavior = .Ignore; | 4946 | child.stderr_behavior = .Pipe; |
| 5031 | child.stderr_behavior = .Pipe; | ||
| 5032 | |||
| 5033 | try child.spawn(); | ||
| 5034 | 4947 | ||
| 5035 | const stderr_reader = child.stderr.?.reader(); | 4948 | try child.spawn(); |
| 5036 | |||
| 5037 | const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024); | ||
| 5038 | |||
| 5039 | const term = child.wait() catch |err| { | ||
| 5040 | return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); | ||
| 5041 | }; | ||
| 5042 | |||
| 5043 | switch (term) { | ||
| 5044 | .Exited => |code| { | ||
| 5045 | if (code != 0) { | ||
| 5046 | // TODO parse clang stderr and turn it into an error message | ||
| 5047 | // and then call failCObjWithOwnedErrorMsg | ||
| 5048 | log.err("clang preprocessor failed with stderr:\n{s}", .{stderr}); | ||
| 5049 | return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{code}); | ||
| 5050 | } | ||
| 5051 | }, | ||
| 5052 | else => { | ||
| 5053 | log.err("clang preprocessor terminated with stderr:\n{s}", .{stderr}); | ||
| 5054 | return comp.failWin32Resource(win32_resource, "clang preprocessor terminated unexpectedly", .{}); | ||
| 5055 | }, | ||
| 5056 | } | ||
| 5057 | } else { | ||
| 5058 | const exit_code = try clangMain(arena, argv.items); | ||
| 5059 | if (exit_code != 0) { | ||
| 5060 | return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{exit_code}); | ||
| 5061 | } | ||
| 5062 | } | ||
| 5063 | 4949 | ||
| 5064 | const dep_basename = std.fs.path.basename(out_dep_path); | 4950 | const stderr_reader = child.stderr.?.reader(); |
| 5065 | // Add the files depended on to the cache system. | 4951 | const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024); |
| 5066 | try man.addDepFilePost(zig_cache_tmp_dir, dep_basename); | 4952 | const term = child.wait() catch |err| { |
| 5067 | switch (comp.cache_use) { | 4953 | return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); |
| 5068 | .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| { | ||
| 5069 | whole.cache_manifest_mutex.lock(); | ||
| 5070 | defer whole.cache_manifest_mutex.unlock(); | ||
| 5071 | try whole_cache_manifest.addDepFilePost(zig_cache_tmp_dir, dep_basename); | ||
| 5072 | }, | ||
| 5073 | .incremental => {}, | ||
| 5074 | } | ||
| 5075 | // Just to save disk space, we delete the file because it is never needed again. | ||
| 5076 | zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| { | ||
| 5077 | log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) }); | ||
| 5078 | }; | 4954 | }; |
| 5079 | 4955 | ||
| 5080 | const full_input = std.fs.cwd().readFileAlloc(arena, out_rcpp_path, std.math.maxInt(usize)) catch |err| switch (err) { | 4956 | switch (term) { |
| 5081 | error.OutOfMemory => return error.OutOfMemory, | 4957 | .Exited => |code| { |
| 5082 | else => |e| { | 4958 | if (code != 0) { |
| 5083 | return comp.failWin32Resource(win32_resource, "failed to read preprocessed file '{s}': {s}", .{ out_rcpp_path, @errorName(e) }); | 4959 | log.err("zig rc failed with stderr:\n{s}", .{stderr}); |
| 4960 | return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code}); | ||
| 4961 | } | ||
| 4962 | }, | ||
| 4963 | else => { | ||
| 4964 | log.err("zig rc terminated with stderr:\n{s}", .{stderr}); | ||
| 4965 | return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); | ||
| 5084 | }, | 4966 | }, |
| 5085 | }; | ||
| 5086 | |||
| 5087 | var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(arena, full_input, full_input, .{ .initial_filename = rc_src.src_path }); | ||
| 5088 | defer mapping_results.mappings.deinit(arena); | ||
| 5089 | |||
| 5090 | const final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); | ||
| 5091 | |||
| 5092 | var output_file = zig_cache_tmp_dir.createFile(out_res_path, .{}) catch |err| { | ||
| 5093 | return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ out_res_path, @errorName(err) }); | ||
| 5094 | }; | ||
| 5095 | var output_file_closed = false; | ||
| 5096 | defer if (!output_file_closed) output_file.close(); | ||
| 5097 | |||
| 5098 | var diagnostics = resinator.errors.Diagnostics.init(arena); | ||
| 5099 | defer diagnostics.deinit(); | ||
| 5100 | |||
| 5101 | var dependencies_list = std.ArrayList([]const u8).init(comp.gpa); | ||
| 5102 | defer { | ||
| 5103 | for (dependencies_list.items) |item| { | ||
| 5104 | comp.gpa.free(item); | ||
| 5105 | } | ||
| 5106 | dependencies_list.deinit(); | ||
| 5107 | } | 4967 | } |
| 5108 | 4968 | ||
| 5109 | var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); | 4969 | // Read depfile and update cache manifest |
| 5110 | 4970 | { | |
| 5111 | resinator.compile.compile(arena, final_input, output_buffered_stream.writer(), .{ | 4971 | const dep_basename = std.fs.path.basename(out_dep_path); |
| 5112 | .cwd = std.fs.cwd(), | 4972 | const dep_file_contents = try zig_cache_tmp_dir.readFileAlloc(arena, dep_basename, 50 * 1024 * 1024); |
| 5113 | .diagnostics = &diagnostics, | 4973 | defer arena.free(dep_file_contents); |
| 5114 | .source_mappings = &mapping_results.mappings, | ||
| 5115 | .dependencies_list = &dependencies_list, | ||
| 5116 | .system_include_paths = comp.rc_include_dir_list, | ||
| 5117 | .ignore_include_env_var = true, | ||
| 5118 | // options | ||
| 5119 | .extra_include_paths = options.extra_include_paths.items, | ||
| 5120 | .default_language_id = options.default_language_id, | ||
| 5121 | .default_code_page = options.default_code_page orelse .windows1252, | ||
| 5122 | .verbose = options.verbose, | ||
| 5123 | .null_terminate_string_table_strings = options.null_terminate_string_table_strings, | ||
| 5124 | .max_string_literal_codepoints = options.max_string_literal_codepoints, | ||
| 5125 | .silent_duplicate_control_ids = options.silent_duplicate_control_ids, | ||
| 5126 | .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, | ||
| 5127 | }) catch |err| switch (err) { | ||
| 5128 | error.ParseError, error.CompileError => { | ||
| 5129 | // Delete the output file on error | ||
| 5130 | output_file.close(); | ||
| 5131 | output_file_closed = true; | ||
| 5132 | // Failing to delete is not really a big deal, so swallow any errors | ||
| 5133 | zig_cache_tmp_dir.deleteFile(out_res_path) catch { | ||
| 5134 | log.warn("failed to delete '{s}': {s}", .{ out_res_path, @errorName(err) }); | ||
| 5135 | }; | ||
| 5136 | return comp.failWin32ResourceCompile(win32_resource, final_input, &diagnostics, mapping_results.mappings); | ||
| 5137 | }, | ||
| 5138 | else => |e| return e, | ||
| 5139 | }; | ||
| 5140 | 4974 | ||
| 5141 | try output_buffered_stream.flush(); | 4975 | const value = try std.json.parseFromSliceLeaky(std.json.Value, arena, dep_file_contents, .{}); |
| 4976 | if (value != .array) { | ||
| 4977 | return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{}); | ||
| 4978 | } | ||
| 5142 | 4979 | ||
| 5143 | for (dependencies_list.items) |dep_file_path| { | 4980 | for (value.array.items) |element| { |
| 5144 | try man.addFilePost(dep_file_path); | 4981 | if (element != .string) { |
| 5145 | switch (comp.cache_use) { | 4982 | return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{}); |
| 5146 | .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| { | 4983 | } |
| 5147 | whole.cache_manifest_mutex.lock(); | 4984 | const dep_file_path = element.string; |
| 5148 | defer whole.cache_manifest_mutex.unlock(); | 4985 | try man.addFilePost(dep_file_path); |
| 5149 | try whole_cache_manifest.addFilePost(dep_file_path); | 4986 | switch (comp.cache_use) { |
| 5150 | }, | 4987 | .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| { |
| 5151 | .incremental => {}, | 4988 | whole.cache_manifest_mutex.lock(); |
| 4989 | defer whole.cache_manifest_mutex.unlock(); | ||
| 4990 | try whole_cache_manifest.addFilePost(dep_file_path); | ||
| 4991 | }, | ||
| 4992 | .incremental => {}, | ||
| 4993 | } | ||
| 5152 | } | 4994 | } |
| 4995 | // Just to save disk space, we delete the file because it is never needed again. | ||
| 4996 | zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| { | ||
| 4997 | log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) }); | ||
| 4998 | }; | ||
| 5153 | } | 4999 | } |
| 5154 | 5000 | ||
| 5155 | // Rename into place. | 5001 | // Rename into place. |
| ... | @@ -5159,8 +5005,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 | ... | @@ -5159,8 +5005,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32 |
| 5159 | defer o_dir.close(); | 5005 | defer o_dir.close(); |
| 5160 | const tmp_basename = std.fs.path.basename(out_res_path); | 5006 | const tmp_basename = std.fs.path.basename(out_res_path); |
| 5161 | try std.fs.rename(zig_cache_tmp_dir, tmp_basename, o_dir, res_filename); | 5007 | try std.fs.rename(zig_cache_tmp_dir, tmp_basename, o_dir, res_filename); |
| 5162 | const tmp_rcpp_basename = std.fs.path.basename(out_rcpp_path); | ||
| 5163 | try std.fs.rename(zig_cache_tmp_dir, tmp_rcpp_basename, o_dir, rcpp_filename); | ||
| 5164 | break :blk digest; | 5008 | break :blk digest; |
| 5165 | }; | 5009 | }; |
| 5166 | 5010 | ||
| ... | @@ -5352,16 +5196,9 @@ pub fn addCCArgs( | ... | @@ -5352,16 +5196,9 @@ pub fn addCCArgs( |
| 5352 | try argv.append("-isystem"); | 5196 | try argv.append("-isystem"); |
| 5353 | try argv.append(c_headers_dir); | 5197 | try argv.append(c_headers_dir); |
| 5354 | 5198 | ||
| 5355 | if (ext == .rc) { | 5199 | for (comp.libc_include_dir_list) |include_dir| { |
| 5356 | for (comp.rc_include_dir_list) |include_dir| { | 5200 | try argv.append("-isystem"); |
| 5357 | try argv.append("-isystem"); | 5201 | try argv.append(include_dir); |
| 5358 | try argv.append(include_dir); | ||
| 5359 | } | ||
| 5360 | } else { | ||
| 5361 | for (comp.libc_include_dir_list) |include_dir| { | ||
| 5362 | try argv.append("-isystem"); | ||
| 5363 | try argv.append(include_dir); | ||
| 5364 | } | ||
| 5365 | } | 5202 | } |
| 5366 | 5203 | ||
| 5367 | if (target.cpu.model.llvm_name) |llvm_name| { | 5204 | if (target.cpu.model.llvm_name) |llvm_name| { |
| ... | @@ -5726,167 +5563,6 @@ fn failWin32ResourceWithOwnedBundle( | ... | @@ -5726,167 +5563,6 @@ fn failWin32ResourceWithOwnedBundle( |
| 5726 | return error.AnalysisFail; | 5563 | return error.AnalysisFail; |
| 5727 | } | 5564 | } |
| 5728 | 5565 | ||
| 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 | pub const FileExt = enum { | 5566 | pub const FileExt = enum { |
| 5891 | c, | 5567 | c, |
| 5892 | cpp, | 5568 | cpp, |
src/main.zig+6-271| ... | @@ -291,7 +291,12 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { | ... | @@ -291,7 +291,12 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { |
| 291 | } else if (mem.eql(u8, cmd, "translate-c")) { | 291 | } else if (mem.eql(u8, cmd, "translate-c")) { |
| 292 | return buildOutputType(gpa, arena, args, .translate_c); | 292 | return buildOutputType(gpa, arena, args, .translate_c); |
| 293 | } else if (mem.eql(u8, cmd, "rc")) { | 293 | } else if (mem.eql(u8, cmd, "rc")) { |
| 294 | return cmdRc(gpa, arena, args[1..]); | 294 | return jitCmd(gpa, arena, cmd_args, .{ |
| 295 | .cmd_name = "resinator", | ||
| 296 | .root_src_path = "resinator/main.zig", | ||
| 297 | .depend_on_aro = true, | ||
| 298 | .prepend_zig_lib_dir_path = true, | ||
| 299 | }); | ||
| 295 | } else if (mem.eql(u8, cmd, "fmt")) { | 300 | } else if (mem.eql(u8, cmd, "fmt")) { |
| 296 | return jitCmd(gpa, arena, cmd_args, .{ | 301 | return jitCmd(gpa, arena, cmd_args, .{ |
| 297 | .cmd_name = "fmt", | 302 | .cmd_name = "fmt", |
| ... | @@ -4625,276 +4630,6 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati | ... | @@ -4625,276 +4630,6 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati |
| 4625 | } | 4630 | } |
| 4626 | } | 4631 | } |
| 4627 | 4632 | ||
| 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 | const usage_init = | 4633 | const usage_init = |
| 4899 | \\Usage: zig init | 4634 | \\Usage: zig init |
| 4900 | \\ | 4635 | \\ |
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 | } | ||