feature. See also
. The project being documented here (as the example) is the Zig library itself.
compile.Compiler
pub const Compiler = struct
File
Code
pub const Compiler = struct {
source: []const u8,
arena: Allocator,
allocator: Allocator,
io: Io,
cwd: std.Io.Dir,
state: State = .{},
diagnostics: *Diagnostics,
dependencies: ?*Dependencies,
input_code_pages: *const CodePageLookup,
output_code_pages: *const CodePageLookup,
search_dirs: []SearchDir,
null_terminate_string_table_strings: bool,
silent_duplicate_control_ids: bool,
pub const State = struct {
icon_id: u16 = 1,
string_tables: StringTablesByLanguage = .{},
language: res.Language = .{},
font_dir: FontDir = .{},
version: u32 = 0,
characteristics: u32 = 0,
};
pub fn writeRoot(self: *Compiler, root: *Node.Root, writer: *std.Io.Writer) !void {
try writeEmptyResource(writer);
for (root.body) |node| {
try self.writeNode(node, writer);
}
try self.state.font_dir.writeResData(self, writer);
if (self.state.font_dir.fonts.items.len != 0) {
// due to it sometimes writing a non-zero-length device name/face name
// whereas we *always* write them both as zero-length.
//
// In practical terms, this doesn't matter, since for various reasons the format
// of the FONTDIR cannot be relied on and is seemingly not actually used by anything
// anymore. We still want to emit some sort of diagnostic for the purposes of being able
// to know that our .RES is intentionally not meant to be byte-for-byte identical with
// the rc.exe output.
//
// By using the hint type here, we allow this diagnostic to be detected in code,
// but it will not be printed since the end-user doesn't need to care.
try self.addErrorDetails(.{
.err = .result_contains_fontdir,
.type = .hint,
.token = .{
.id = .invalid,
.start = 0,
.end = 0,
.line_number = 1,
},
});
}
var string_tables_it = self.state.string_tables.tables.iterator();
while (string_tables_it.next()) |string_table_entry| {
var string_table_it = string_table_entry.value_ptr.blocks.iterator();
while (string_table_it.next()) |entry| {
try entry.value_ptr.writeResData(self, string_table_entry.key_ptr.*, entry.key_ptr.*, writer);
}
}
}
pub fn writeNode(self: *Compiler, node: *Node, writer: *std.Io.Writer) !void {
switch (node.id) {
.root => unreachable,
.resource_external => try self.writeResourceExternal(@alignCast(@fieldParentPtr("base", node)), writer),
.resource_raw_data => try self.writeResourceRawData(@alignCast(@fieldParentPtr("base", node)), writer),
.literal => unreachable,
.binary_expression => unreachable,
.grouped_expression => unreachable,
.not_expression => unreachable,
.invalid => {},
.accelerators => try self.writeAccelerators(@alignCast(@fieldParentPtr("base", node)), writer),
.accelerator => unreachable,
.dialog => try self.writeDialog(@alignCast(@fieldParentPtr("base", node)), writer),
.control_statement => unreachable,
.toolbar => try self.writeToolbar(@alignCast(@fieldParentPtr("base", node)), writer),
.menu => try self.writeMenu(@alignCast(@fieldParentPtr("base", node)), writer),
.menu_item => unreachable,
.menu_item_separator => unreachable,
.menu_item_ex => unreachable,
.popup => unreachable,
.popup_ex => unreachable,
.version_info => try self.writeVersionInfo(@alignCast(@fieldParentPtr("base", node)), writer),
.version_statement => unreachable,
.block => unreachable,
.block_value => unreachable,
.block_value_value => unreachable,
.string_table => try self.writeStringTable(@alignCast(@fieldParentPtr("base", node))),
.string_table_string => unreachable,
.language_statement => self.writeLanguageStatement(@alignCast(@fieldParentPtr("base", node))),
.font_statement => unreachable,
.simple_statement => self.writeTopLevelSimpleStatement(@alignCast(@fieldParentPtr("base", node))),
}
}
pub fn evaluateFilenameExpression(self: *Compiler, expression_node: *Node) ![]u8 {
switch (expression_node.id) {
.literal => {
const literal_node = expression_node.cast(.literal).?;
switch (literal_node.token.id) {
.literal, .number => {
const slice = literal_node.token.slice(self.source);
const code_page = self.input_code_pages.getForToken(literal_node.token);
var buf = try std.ArrayList(u8).initCapacity(self.allocator, slice.len);
errdefer buf.deinit(self.allocator);
var index: usize = 0;
while (code_page.codepointAt(index, slice)) |codepoint| : (index += codepoint.byte_len) {
const c = codepoint.value;
if (c == code_pages.Codepoint.invalid) {
try buf.appendSlice(self.allocator, "�");
} else {
const utf8_len = std.unicode.utf8CodepointSequenceLength(c) catch unreachable;
try buf.ensureUnusedCapacity(self.allocator, utf8_len);
_ = std.unicode.utf8Encode(c, buf.unusedCapacitySlice()) catch unreachable;
buf.items.len += utf8_len;
}
}
return buf.toOwnedSlice(self.allocator);
},
.quoted_ascii_string, .quoted_wide_string => {
const slice = literal_node.token.slice(self.source);
const column = literal_node.token.calculateColumn(self.source, 8, null);
const bytes = SourceBytes{ .slice = slice, .code_page = self.input_code_pages.getForToken(literal_node.token) };
var buf: std.ArrayList(u8) = .empty;
errdefer buf.deinit(self.allocator);
// hex/octal escapes is still determined by the L prefix. Since we want to end up with
// UTF-8, we can parse either string type directly to UTF-8.
var parser = literals.IterativeStringParser.init(bytes, .{
.start_column = column,
.diagnostics = self.errContext(literal_node.token),
// output code page would make more sense.
.output_code_page = .windows1252,
});
while (try parser.nextUnchecked()) |parsed| {
const c = parsed.codepoint;
if (c == code_pages.Codepoint.invalid) {
try buf.appendSlice(self.allocator, "�");
} else {
var codepoint_buf: [4]u8 = undefined;
if (std.unicode.utf8Encode(c, &codepoint_buf)) |len| {
try buf.appendSlice(self.allocator, codepoint_buf[0..len]);
} else |_| {
try buf.appendSlice(self.allocator, "�");
}
}
}
return buf.toOwnedSlice(self.allocator);
},
else => unreachable,
}
},
.binary_expression => {
const binary_expression_node = expression_node.cast(.binary_expression).?;
return self.evaluateFilenameExpression(binary_expression_node.right);
},
.grouped_expression => {
const grouped_expression_node = expression_node.cast(.grouped_expression).?;
return self.evaluateFilenameExpression(grouped_expression_node.expression);
},
else => unreachable,
}
}
fn searchForFile(self: *Compiler, path: []const u8) !std.Io.File {
const io = self.io;
// paths, so there's no point in checking them.
//
// This behavior was determined/confirmed with the following test:
// - A `test.rc` file with the contents `1 RCDATA "/test.bin"`
// - A `test.bin` file at `C:\test.bin`
// - A `test.bin` file at `inc\test.bin` relative to the .rc file
// - Invoking `rc` with `rc /i inc test.rc`
//
// This results in a .res file with the contents of `C:\test.bin`, not
// the contents of `inc\test.bin`. Further, if `C:\test.bin` is deleted,
// then it start failing to find `/test.bin`, meaning that it does not resolve
// `/test.bin` relative to include paths and instead only treats it as
// an absolute path.
if (std.fs.path.isAbsolute(path)) {
const file = try Io.Dir.cwd().openFile(io, path, .{ .allow_directory = false });
errdefer file.close(io);
if (self.dependencies) |dependencies| {
const duped_path = try dependencies.allocator.dupe(u8, path);
errdefer dependencies.allocator.free(duped_path);
try dependencies.list.append(dependencies.allocator, duped_path);
}
}
var first_error: ?(std.Io.File.OpenError || std.Io.File.StatError) = null;
for (self.search_dirs) |search_dir| {
if (search_dir.dir.openFile(io, path, .{ .allow_directory = false })) |file| {
errdefer file.close(io);
if (self.dependencies) |dependencies| {
const searched_file_path = try std.fs.path.join(dependencies.allocator, &.{
search_dir.path orelse "", path,
});
errdefer dependencies.allocator.free(searched_file_path);
try dependencies.list.append(dependencies.allocator, searched_file_path);
}
return file;
} else |err| if (first_error == null) {
first_error = err;
}
}
return first_error orelse error.FileNotFound;
}
pub fn parseDlgIncludeString(self: *Compiler, token: Token) ![]u8 {
const bytes = self.sourceBytesForToken(token);
const output_code_page = self.output_code_pages.getForToken(token);
var buf = try std.ArrayList(u8).initCapacity(self.allocator, bytes.slice.len);
errdefer buf.deinit(self.allocator);
var iterative_parser = literals.IterativeStringParser.init(bytes, .{
.start_column = token.calculateColumn(self.source, 8, null),
.diagnostics = self.errContext(token),
// using the actual output code page would make more sense.
.output_code_page = .windows1252,
});
// encoded as Windows-1252. This effectively consolidates the two-step process
// of rc.exe into one step, since rc.exe's preprocessor converts to UTF-16 (this
// is when invalid sequences are replaced by the replacement character (U+FFFD)),
// and then that's run through the parser. Our preprocessor keeps things in their
// original encoding, meaning we emulate the <encoding> -> UTF-16 -> Windows-1252
// results all at once.
while (try iterative_parser.next()) |parsed| {
const c = parsed.codepoint;
switch (iterative_parser.declared_string_type) {
.wide => {
if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
try buf.append(self.allocator, best_fit);
} else if (c < 0x10000 or c == code_pages.Codepoint.invalid or parsed.escaped_surrogate_pair) {
try buf.append(self.allocator, '?');
} else {
try buf.appendSlice(self.allocator, "??");
}
},
.ascii => {
if (parsed.from_escaped_integer) {
const truncated: u8 = @truncate(c);
switch (output_code_page) {
.utf8 => switch (truncated) {
0...0x7F => try buf.append(self.allocator, truncated),
else => try buf.append(self.allocator, '?'),
},
.windows1252 => {
try buf.append(self.allocator, truncated);
},
}
} else {
if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
try buf.append(self.allocator, best_fit);
} else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
try buf.append(self.allocator, '?');
} else {
try buf.appendSlice(self.allocator, "??");
}
}
},
}
}
return buf.toOwnedSlice(self.allocator);
}
pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: *std.Io.Writer) !void {
const io = self.io;
var header = try self.resourceHeader(node.id, node.type, .{});
defer header.deinit(self.allocator);
const maybe_predefined_type = header.predefinedResourceType();
if (maybe_predefined_type != null and maybe_predefined_type.? == .DLGINCLUDE) {
const filename_token = node.filename.cast(.literal).?.token;
const parsed_filename = try self.parseDlgIncludeString(filename_token);
defer self.allocator.free(parsed_filename);
const parsed_filename_terminated = std.mem.sliceTo(parsed_filename, 0);
header.applyMemoryFlags(node.common_resource_attributes, self.source);
// Each codepoint within a DLGINCLUDE string is encoded as a maximum of
// 2 bytes, which means that the maximum byte length of a DLGINCLUDE string is
// (including the NUL terminator): 32,767 * 2 + 1 = 65,535 or exactly the u16 max.
header.data_size = @intCast(parsed_filename_terminated.len + 1);
try header.write(writer, self.errContext(node.id));
try writer.writeAll(parsed_filename_terminated);
try writer.writeByte(0);
try writeDataPadding(writer, header.data_size);
return;
}
const filename_utf8 = try self.evaluateFilenameExpression(node.filename);
defer self.allocator.free(filename_utf8);
// This currently only checks for NUL bytes, but it should probably also check for
// platform-specific invalid characters like '*', '?', '"', '<', '>', '|' (Windows)
// Related: https://github.com/ziglang/zig/pull/14533#issuecomment-1416888193
if (std.mem.indexOfScalar(u8, filename_utf8, 0) != null) {
return self.addErrorDetailsAndFail(.{
.err = .invalid_filename,
.token = node.filename.getFirstToken(),
.token_span_end = node.filename.getLastToken(),
.extra = .{ .number = 0 },
});
}
// and almost certainly lead to things not intended by the user (e.g. '(1+-1)' evaluates
// to the filename '-1'), so error if the filename node is a grouped/binary expression.
// Note: This is done here instead of during parsing so that we can easily include
// the evaluated filename as part of the error messages.
if (node.filename.id != .literal) {
const filename_string_index = try self.diagnostics.putString(filename_utf8);
try self.addErrorDetails(.{
.err = .number_expression_as_filename,
.token = node.filename.getFirstToken(),
.token_span_end = node.filename.getLastToken(),
.extra = .{ .number = filename_string_index },
});
return self.addErrorDetailsAndFail(.{
.err = .number_expression_as_filename,
.type = .note,
.token = node.filename.getFirstToken(),
.token_span_end = node.filename.getLastToken(),
.print_source_line = false,
.extra = .{ .number = filename_string_index },
});
}
// so get it here to simplify future usage.
const filename_token = node.filename.getFirstToken();
const file_handle = self.searchForFile(filename_utf8) catch |err| switch (err) {
error.OutOfMemory => |e| return e,
else => |e| {
const filename_string_index = try self.diagnostics.putString(filename_utf8);
return self.addErrorDetailsAndFail(.{
.err = .file_open_error,
.token = filename_token,
.extra = .{ .file_open_error = .{
.err = ErrorDetails.FileOpenError.enumFromError(e),
.filename_string_index = filename_string_index,
} },
});
},
};
defer file_handle.close(io);
var file_buffer: [2048]u8 = undefined;
var file_reader = file_handle.reader(io, &file_buffer);
if (maybe_predefined_type) |predefined_type| {
switch (predefined_type) {
.GROUP_ICON, .GROUP_CURSOR => {
if (ani.isAnimatedIcon(&file_reader.interface)) {
// and the resource type changes to ANIICON/ANICURSOR
const new_predefined_type: res.RT = switch (predefined_type) {
.GROUP_ICON => .ANIICON,
.GROUP_CURSOR => .ANICURSOR,
else => unreachable,
};
header.type_value.ordinal = @backingInt(new_predefined_type);
header.memory_flags = MemoryFlags.defaults(new_predefined_type);
header.applyMemoryFlags(node.common_resource_attributes, self.source);
header.data_size = std.math.cast(u32, try file_reader.getSize()) orelse {
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
};
try header.write(writer, self.errContext(node.id));
try file_reader.seekTo(0);
try writeResourceData(writer, &file_reader.interface, header.data_size);
return;
}
try file_reader.seekTo(0);
const icon_dir = ico.read(self.allocator, &file_reader.interface, try file_reader.getSize()) catch |err| switch (err) {
error.OutOfMemory => |e| return e,
else => |e| {
return self.iconReadError(
e,
filename_utf8,
filename_token,
predefined_type,
);
},
};
defer icon_dir.deinit();
std.debug.assert(icon_dir.entries.len <= std.math.maxInt(u16));
// in the icon_dir regardless of the type of resource specified in the .rc.
// This leads to unusable .res files when the types mismatch, so
// we error instead.
const res_types_match = switch (predefined_type) {
.GROUP_ICON => icon_dir.image_type == .icon,
.GROUP_CURSOR => icon_dir.image_type == .cursor,
else => unreachable,
};
if (!res_types_match) {
return self.addErrorDetailsAndFail(.{
.err = .icon_dir_and_resource_type_mismatch,
.token = filename_token,
.extra = .{ .resource = switch (predefined_type) {
.GROUP_ICON => .icon,
.GROUP_CURSOR => .cursor,
else => unreachable,
} },
});
}
var icon_memory_flags = MemoryFlags.defaults(res.RT.ICON);
applyToMemoryFlags(&icon_memory_flags, node.common_resource_attributes, self.source);
applyToGroupMemoryFlags(&header.memory_flags, node.common_resource_attributes, self.source);
const first_icon_id = self.state.icon_id;
const entry_type = if (predefined_type == .GROUP_ICON) @backingInt(res.RT.ICON) else @backingInt(res.RT.CURSOR);
for (icon_dir.entries, 0..) |*entry, entry_i_usize| {
// cast it here to simplify usage sites.
const entry_i: u16 = @intCast(entry_i_usize);
var full_data_size = entry.data_size_in_bytes;
if (icon_dir.image_type == .cursor) {
full_data_size = std.math.add(u32, full_data_size, 4) catch {
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
};
}
const image_header = ResourceHeader{
.type_value = .{ .ordinal = entry_type },
.name_value = .{ .ordinal = self.state.icon_id },
.data_size = full_data_size,
.memory_flags = icon_memory_flags,
.language = self.state.language,
.version = self.state.version,
.characteristics = self.state.characteristics,
};
try image_header.write(writer, self.errContext(node.id));
// > The LOCALHEADER structure is the first data written to the RT_CURSOR
// > resource if a RESDIR structure contains information about a cursor.
// where LOCALHEADER is `struct { WORD xHotSpot; WORD yHotSpot; }`
if (icon_dir.image_type == .cursor) {
try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_x, .little);
try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_y, .little);
}
try file_reader.seekTo(entry.data_offset_from_start_of_file);
var header_bytes: [16]u8 align(@alignOf(ico.BitmapHeader)) = (file_reader.interface.takeArray(16) catch {
return self.iconReadError(
error.UnexpectedEOF,
filename_utf8,
filename_token,
predefined_type,
);
}).*;
const image_format = ico.ImageFormat.detect(&header_bytes);
if (!image_format.validate(&header_bytes)) {
return self.iconReadError(
error.InvalidHeader,
filename_utf8,
filename_token,
predefined_type,
);
}
switch (image_format) {
.riff => switch (icon_dir.image_type) {
.icon => {
// with RIFF encoded icons within them work ~okay (they work
// in some places but not others, they may not animate, etc) if they are
// allowed to be compiled.
try self.addErrorDetails(.{
.err = .rc_would_error_on_icon_dir,
.type = .warning,
.token = filename_token,
.extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
});
try self.addErrorDetails(.{
.err = .rc_would_error_on_icon_dir,
.type = .note,
.print_source_line = false,
.token = filename_token,
.extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
});
},
.cursor => {
// here because the cursor would fail to be loaded at runtime if we
// compiled it.
return self.addErrorDetailsAndFail(.{
.err = .format_not_supported_in_icon_dir,
.token = filename_token,
.extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .riff, .index = entry_i } },
});
},
},
.png => switch (icon_dir.image_type) {
.icon => {
entry.type_specific_data.icon.color_planes = 1;
// get treated specially
entry.type_specific_data.icon.bits_per_pixel = switch (entry.num_colors) {
2 => 1,
8 => 3,
16 => 4,
else => entry.type_specific_data.icon.bits_per_pixel,
};
},
.cursor => {
// with PNG encoded icons within them work fine if they are
// allowed to be compiled.
try self.addErrorDetails(.{
.err = .rc_would_error_on_icon_dir,
.type = .warning,
.token = filename_token,
.extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .png, .index = entry_i } },
});
},
},
.dib => {
const bitmap_header: *ico.BitmapHeader = @ptrCast(@alignCast(&header_bytes));
if (native_endian == .big) {
std.mem.byteSwapAllFields(ico.BitmapHeader, bitmap_header);
}
const bitmap_version = ico.BitmapHeader.Version.get(bitmap_header.bcSize);
// `bcSize == sizeof(BITMAPINFOHEADER)`, but it seems unlikely
// that there's a good reason for that outside of too-old
// bitmap headers.
// TODO: Need to test V4 and V5 bitmaps to check they actually work
if (bitmap_version == .@"win2.0") {
return self.addErrorDetailsAndFail(.{
.err = .rc_would_error_on_bitmap_version,
.token = filename_token,
.extra = .{ .icon_dir = .{
.icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
.icon_format = image_format,
.index = entry_i,
.bitmap_version = bitmap_version,
} },
});
} else if (bitmap_version != .@"nt3.1") {
try self.addErrorDetails(.{
.err = .rc_would_error_on_bitmap_version,
.type = .warning,
.token = filename_token,
.extra = .{ .icon_dir = .{
.icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
.icon_format = image_format,
.index = entry_i,
.bitmap_version = bitmap_version,
} },
});
}
switch (icon_dir.image_type) {
.icon => {
// the values in the IconDir, but not in the LOCALHEADER (see above).
entry.type_specific_data.icon.color_planes = bitmap_header.bcPlanes;
entry.type_specific_data.icon.bits_per_pixel = bitmap_header.bcBitCount;
},
.cursor => {
entry.width = @intCast(bitmap_header.bcWidth);
entry.height = @intCast(bitmap_header.bcHeight);
entry.type_specific_data.cursor.hotspot_x = bitmap_header.bcPlanes;
entry.type_specific_data.cursor.hotspot_y = bitmap_header.bcBitCount;
},
}
},
}
try file_reader.seekTo(entry.data_offset_from_start_of_file);
try writeResourceDataNoPadding(writer, &file_reader.interface, entry.data_size_in_bytes);
try writeDataPadding(writer, full_data_size);
if (self.state.icon_id == std.math.maxInt(u16)) {
try self.addErrorDetails(.{
.err = .max_icon_ids_exhausted,
.print_source_line = false,
.token = filename_token,
.extra = .{ .icon_dir = .{
.icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
.icon_format = image_format,
.index = entry_i,
} },
});
return self.addErrorDetailsAndFail(.{
.err = .max_icon_ids_exhausted,
.type = .note,
.token = filename_token,
.extra = .{ .icon_dir = .{
.icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
.icon_format = image_format,
.index = entry_i,
} },
});
}
self.state.icon_id += 1;
}
header.data_size = icon_dir.getResDataSize();
try header.write(writer, self.errContext(node.id));
try icon_dir.writeResData(writer, first_icon_id);
try writeDataPadding(writer, header.data_size);
return;
},
.RCDATA,
.HTML,
.MESSAGETABLE,
.DLGINIT,
.PLUGPLAY,
.VXD,
// as the resource type.
.MANIFEST,
.CURSOR,
.ICON,
.ANICURSOR,
.ANIICON,
.FONTDIR,
=> {
header.applyMemoryFlags(node.common_resource_attributes, self.source);
},
.BITMAP => {
header.applyMemoryFlags(node.common_resource_attributes, self.source);
const file_size = try file_reader.getSize();
const bitmap_info = bmp.read(&file_reader.interface, file_size) catch |err| {
const filename_string_index = try self.diagnostics.putString(filename_utf8);
return self.addErrorDetailsAndFail(.{
.err = .bmp_read_error,
.token = filename_token,
.extra = .{ .bmp_read_error = .{
.err = ErrorDetails.BitmapReadError.enumFromError(err),
.filename_string_index = filename_string_index,
} },
});
};
if (bitmap_info.getActualPaletteByteLen() > bitmap_info.getExpectedPaletteByteLen()) {
const num_ignored_bytes = bitmap_info.getActualPaletteByteLen() - bitmap_info.getExpectedPaletteByteLen();
var number_as_bytes: [8]u8 = undefined;
std.mem.writeInt(u64, &number_as_bytes, num_ignored_bytes, native_endian);
const value_string_index = try self.diagnostics.putString(&number_as_bytes);
try self.addErrorDetails(.{
.err = .bmp_ignored_palette_bytes,
.type = .warning,
.token = filename_token,
.extra = .{ .number = value_string_index },
});
} else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {
const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();
var number_as_bytes: [8]u8 = undefined;
std.mem.writeInt(u64, &number_as_bytes, num_padding_bytes, native_endian);
const value_string_index = try self.diagnostics.putString(&number_as_bytes);
try self.addErrorDetails(.{
.err = .bmp_missing_palette_bytes,
.type = .err,
.token = filename_token,
.extra = .{ .number = value_string_index },
});
const pixel_data_len = bitmap_info.getPixelDataLen(file_size);
// at least one entry to the diagnostics strings, so we can
// get away with using 0 to mean 'no string' here.
var miscompiled_bytes_string_index: u32 = 0;
if (pixel_data_len > 0) {
const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);
std.mem.writeInt(u64, &number_as_bytes, miscompiled_bytes, native_endian);
miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);
}
return self.addErrorDetailsAndFail(.{
.err = .rc_would_miscompile_bmp_palette_padding,
.type = .note,
.print_source_line = false,
.token = filename_token,
.extra = .{ .number = miscompiled_bytes_string_index },
});
}
// could underflow if the underlying file is modified while reading
// it, but need to think about it more to determine if that's a
// real possibility
const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));
header.data_size = bmp_bytes_to_write;
try header.write(writer, self.errContext(node.id));
try file_reader.seekTo(bmp.file_header_len);
try writeResourceDataNoPadding(writer, &file_reader.interface, bitmap_info.dib_header_size);
if (bitmap_info.getBitmasksByteLen() > 0) {
try writeResourceDataNoPadding(writer, &file_reader.interface, bitmap_info.getBitmasksByteLen());
}
if (bitmap_info.getExpectedPaletteByteLen() > 0) {
try writeResourceDataNoPadding(writer, &file_reader.interface, @intCast(bitmap_info.getActualPaletteByteLen()));
}
try file_reader.seekTo(bitmap_info.pixel_data_offset);
const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);
try writeResourceDataNoPadding(writer, &file_reader.interface, pixel_bytes);
try writeDataPadding(writer, bmp_bytes_to_write);
return;
},
.FONT => {
if (self.state.font_dir.ids.get(header.name_value.ordinal) != null) {
// Note: The Win32 compiler prints this as an error but it doesn't fail the compilation
// and the duplicate resource is skipped.
try self.addErrorDetails(.{
.err = .font_id_already_defined,
.token = node.id,
.type = .warning,
.extra = .{ .number = header.name_value.ordinal },
});
try self.addErrorDetails(.{
.err = .font_id_already_defined,
.token = self.state.font_dir.ids.get(header.name_value.ordinal).?,
.type = .note,
.extra = .{ .number = header.name_value.ordinal },
});
return;
}
header.applyMemoryFlags(node.common_resource_attributes, self.source);
const file_size = try file_reader.getSize();
if (file_size > std.math.maxInt(u32)) {
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
}
header.data_size = @intCast(file_size);
try header.write(writer, self.errContext(node.id));
var font_dir_header_buf: [148]u8 = @splat(0);
const populated_len: u32 = @intCast(try file_reader.interface.readSliceShort(&font_dir_header_buf));
try writer.writeAll(font_dir_header_buf[0..populated_len]);
try writeResourceDataNoPadding(writer, &file_reader.interface, header.data_size - populated_len);
try writeDataPadding(writer, header.data_size);
try self.state.font_dir.add(self.arena, FontDir.Font{
.id = header.name_value.ordinal,
.header_bytes = font_dir_header_buf,
}, node.id);
return;
},
.ACCELERATOR,
.DIALOG,
.DLGINCLUDE,
.MENU,
.STRING,
.TOOLBAR,
.VERSION,
=> unreachable,
_ => unreachable,
}
} else {
header.applyMemoryFlags(node.common_resource_attributes, self.source);
}
const data_size = try file_reader.getSize();
if (data_size > std.math.maxInt(u32)) {
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
}
header.data_size = @intCast(data_size);
try header.write(writer, self.errContext(node.id));
try writeResourceData(writer, &file_reader.interface, header.data_size);
}
fn iconReadError(
self: *Compiler,
err: ico.ReadError,
filename: []const u8,
token: Token,
predefined_type: res.RT,
) error{ CompileError, OutOfMemory } {
const filename_string_index = try self.diagnostics.putString(filename);
return self.addErrorDetailsAndFail(.{
.err = .icon_read_error,
.token = token,
.extra = .{ .icon_read_error = .{
.err = ErrorDetails.IconReadError.enumFromError(err),
.icon_type = switch (predefined_type) {
.GROUP_ICON => .icon,
.GROUP_CURSOR => .cursor,
else => unreachable,
},
.filename_string_index = filename_string_index,
} },
});
}
pub const DataType = enum {
number,
ascii_string,
wide_string,
};
pub const Data = union(DataType) {
number: Number,
ascii_string: []const u8,
wide_string: [:0]const u16,
pub fn deinit(self: Data, allocator: Allocator) void {
switch (self) {
.wide_string => |wide_string| {
allocator.free(wide_string);
},
.ascii_string => |ascii_string| {
allocator.free(ascii_string);
},
else => {},
}
}
pub fn write(self: Data, writer: *std.Io.Writer) !void {
switch (self) {
.number => |number| switch (number.is_long) {
false => try writer.writeInt(WORD, number.asWord(), .little),
true => try writer.writeInt(DWORD, number.value, .little),
},
.ascii_string => |ascii_string| {
try writer.writeAll(ascii_string);
},
.wide_string => |wide_string| {
try writer.writeAll(std.mem.sliceAsBytes(wide_string));
},
}
}
};
pub fn evaluateNumberExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) Number {
switch (expression_node.id) {
.literal => {
const literal_node = expression_node.cast(.literal).?;
std.debug.assert(literal_node.token.id == .number);
const bytes = SourceBytes{
.slice = literal_node.token.slice(source),
.code_page = code_page_lookup.getForToken(literal_node.token),
};
return literals.parseNumberLiteral(bytes);
},
.binary_expression => {
const binary_expression_node = expression_node.cast(.binary_expression).?;
const lhs = evaluateNumberExpression(binary_expression_node.left, source, code_page_lookup);
const rhs = evaluateNumberExpression(binary_expression_node.right, source, code_page_lookup);
const operator_char = binary_expression_node.operator.slice(source)[0];
return lhs.evaluateOperator(operator_char, rhs);
},
.grouped_expression => {
const grouped_expression_node = expression_node.cast(.grouped_expression).?;
return evaluateNumberExpression(grouped_expression_node.expression, source, code_page_lookup);
},
else => unreachable,
}
}
const FlagsNumber = struct {
value: u32,
not_mask: u32 = 0xFFFFFFFF,
pub fn evaluateOperator(lhs: FlagsNumber, operator_char: u8, rhs: FlagsNumber) FlagsNumber {
const result = switch (operator_char) {
'-' => lhs.value -% rhs.value,
'+' => lhs.value +% rhs.value,
'|' => lhs.value | rhs.value,
'&' => lhs.value & rhs.value,
else => unreachable,
};
return .{
.value = result,
.not_mask = lhs.not_mask & rhs.not_mask,
};
}
pub fn applyNotMask(self: FlagsNumber) u32 {
return self.value & self.not_mask;
}
};
pub fn evaluateFlagsExpressionWithDefault(default: u32, expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) u32 {
var context = FlagsExpressionContext{ .initial_value = default };
const number = evaluateFlagsExpression(expression_node, source, code_page_lookup, &context);
return number.value;
}
pub const FlagsExpressionContext = struct {
initial_value: u32 = 0,
initial_value_used: bool = false,
};
pub fn evaluateFlagsExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup, context: *FlagsExpressionContext) FlagsNumber {
switch (expression_node.id) {
.literal => {
const literal_node = expression_node.cast(.literal).?;
std.debug.assert(literal_node.token.id == .number);
const bytes = SourceBytes{
.slice = literal_node.token.slice(source),
.code_page = code_page_lookup.getForToken(literal_node.token),
};
var value = literals.parseNumberLiteral(bytes).value;
if (!context.initial_value_used) {
context.initial_value_used = true;
value |= context.initial_value;
}
return .{ .value = value };
},
.binary_expression => {
const binary_expression_node = expression_node.cast(.binary_expression).?;
const lhs = evaluateFlagsExpression(binary_expression_node.left, source, code_page_lookup, context);
const rhs = evaluateFlagsExpression(binary_expression_node.right, source, code_page_lookup, context);
const operator_char = binary_expression_node.operator.slice(source)[0];
const result = lhs.evaluateOperator(operator_char, rhs);
return .{ .value = result.applyNotMask() };
},
.grouped_expression => {
const grouped_expression_node = expression_node.cast(.grouped_expression).?;
return evaluateFlagsExpression(grouped_expression_node.expression, source, code_page_lookup, context);
},
.not_expression => {
const not_expression = expression_node.cast(.not_expression).?;
const bytes = SourceBytes{
.slice = not_expression.number_token.slice(source),
.code_page = code_page_lookup.getForToken(not_expression.number_token),
};
const not_number = literals.parseNumberLiteral(bytes);
if (!context.initial_value_used) {
context.initial_value_used = true;
return .{ .value = context.initial_value & ~not_number.value };
}
return .{ .value = 0, .not_mask = ~not_number.value };
},
else => unreachable,
}
}
pub fn evaluateDataExpression(self: *Compiler, expression_node: *Node) !Data {
switch (expression_node.id) {
.literal => {
const literal_node = expression_node.cast(.literal).?;
switch (literal_node.token.id) {
.number => {
const number = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
return .{ .number = number };
},
.quoted_ascii_string => {
const column = literal_node.token.calculateColumn(self.source, 8, null);
const bytes = SourceBytes{
.slice = literal_node.token.slice(self.source),
.code_page = self.input_code_pages.getForToken(literal_node.token),
};
const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{
.start_column = column,
.diagnostics = self.errContext(literal_node.token),
.output_code_page = self.output_code_pages.getForToken(literal_node.token),
});
errdefer self.allocator.free(parsed);
return .{ .ascii_string = parsed };
},
.quoted_wide_string => {
const column = literal_node.token.calculateColumn(self.source, 8, null);
const bytes = SourceBytes{
.slice = literal_node.token.slice(self.source),
.code_page = self.input_code_pages.getForToken(literal_node.token),
};
const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{
.start_column = column,
.diagnostics = self.errContext(literal_node.token),
.output_code_page = self.output_code_pages.getForToken(literal_node.token),
});
errdefer self.allocator.free(parsed_string);
return .{ .wide_string = parsed_string };
},
else => unreachable,
}
},
.binary_expression, .grouped_expression => {
const result = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
return .{ .number = result };
},
.not_expression => unreachable,
else => unreachable,
}
}
pub fn writeResourceRawData(self: *Compiler, node: *Node.ResourceRawData, writer: *std.Io.Writer) !void {
var data_buffer: std.Io.Writer.Allocating = .init(self.allocator);
defer data_buffer.deinit();
for (node.raw_data) |expression| {
const data = try self.evaluateDataExpression(expression);
defer data.deinit(self.allocator);
try data.write(&data_buffer.writer);
}
const data_len: u32 = std.math.cast(u32, data_buffer.written().len) orelse {
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
};
try self.writeResourceHeader(writer, node.id, node.type, data_len, node.common_resource_attributes, self.state.language);
var data_fbs: std.Io.Reader = .fixed(data_buffer.written());
try writeResourceData(writer, &data_fbs, data_len);
}
pub fn writeResourceHeader(self: *Compiler, writer: *std.Io.Writer, id_token: Token, type_token: Token, data_size: u32, common_resource_attributes: []Token, language: res.Language) !void {
var header = try self.resourceHeader(id_token, type_token, .{
.language = language,
.data_size = data_size,
});
defer header.deinit(self.allocator);
header.applyMemoryFlags(common_resource_attributes, self.source);
try header.write(writer, self.errContext(id_token));
}
pub fn writeResourceDataNoPadding(writer: *std.Io.Writer, data_reader: *std.Io.Reader, data_size: u32) !void {
try data_reader.streamExact(writer, data_size);
}
pub fn writeResourceData(writer: *std.Io.Writer, data_reader: *std.Io.Reader, data_size: u32) !void {
try writeResourceDataNoPadding(writer, data_reader, data_size);
try writeDataPadding(writer, data_size);
}
pub fn writeDataPadding(writer: *std.Io.Writer, data_size: u32) !void {
try writer.splatByteAll(0, numPaddingBytesNeeded(data_size));
}
pub fn numPaddingBytesNeeded(data_size: u32) u2 {
return @intCast((4 -% data_size) % 4);
}
pub fn evaluateAcceleratorKeyExpression(self: *Compiler, node: *Node, is_virt: bool) !u16 {
if (node.isNumberExpression()) {
return evaluateNumberExpression(node, self.source, self.input_code_pages).asWord();
} else {
std.debug.assert(node.isStringLiteral());
const literal: *Node.Literal = @alignCast(@fieldParentPtr("base", node));
const bytes = SourceBytes{
.slice = literal.token.slice(self.source),
.code_page = self.input_code_pages.getForToken(literal.token),
};
const column = literal.token.calculateColumn(self.source, 8, null);
return res.parseAcceleratorKeyString(bytes, is_virt, .{
.start_column = column,
.diagnostics = self.errContext(literal.token),
.output_code_page = self.output_code_pages.getForToken(literal.token),
});
}
}
pub fn writeAccelerators(self: *Compiler, node: *Node.Accelerators, writer: *std.Io.Writer) !void {
var data_buffer: std.Io.Writer.Allocating = .init(self.allocator);
defer data_buffer.deinit();
try self.writeAcceleratorsData(node, &data_buffer.writer);
const data_size: u32 = std.math.cast(u32, data_buffer.written().len) orelse {
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
};
var header = try self.resourceHeader(node.id, node.type, .{
.data_size = data_size,
});
defer header.deinit(self.allocator);
header.applyMemoryFlags(node.common_resource_attributes, self.source);
header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
try header.write(writer, self.errContext(node.id));
var data_fbs: std.Io.Reader = .fixed(data_buffer.written());
try writeResourceData(writer, &data_fbs, data_size);
}
pub fn writeAcceleratorsData(self: *Compiler, node: *Node.Accelerators, data_writer: *std.Io.Writer) !void {
for (node.accelerators, 0..) |accel_node, i| {
const accelerator: *Node.Accelerator = @alignCast(@fieldParentPtr("base", accel_node));
var modifiers = res.AcceleratorModifiers{};
for (accelerator.type_and_options) |type_or_option| {
const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;
modifiers.apply(modifier);
}
if ((modifiers.isSet(.control) or modifiers.isSet(.shift)) and !modifiers.isSet(.virtkey)) {
try self.addErrorDetails(.{
.err = .accelerator_shift_or_control_without_virtkey,
.type = .warning,
// in this list.
.token = accelerator.type_and_options[0],
.token_span_end = accelerator.type_and_options[accelerator.type_and_options.len - 1],
});
}
if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {
return self.addErrorDetailsAndFail(.{
.err = .accelerator_type_required,
.token = accelerator.event.getFirstToken(),
.token_span_end = accelerator.event.getLastToken(),
});
}
const key = self.evaluateAcceleratorKeyExpression(accelerator.event, modifiers.isSet(.virtkey)) catch |err| switch (err) {
error.OutOfMemory => |e| return e,
else => |e| {
return self.addErrorDetailsAndFail(.{
.err = .invalid_accelerator_key,
.token = accelerator.event.getFirstToken(),
.token_span_end = accelerator.event.getLastToken(),
.extra = .{ .accelerator_error = .{
.err = ErrorDetails.AcceleratorError.enumFromError(e),
} },
});
},
};
const cmd_id = evaluateNumberExpression(accelerator.idvalue, self.source, self.input_code_pages);
if (i == node.accelerators.len - 1) {
modifiers.markLast();
}
try data_writer.writeByte(modifiers.value);
try data_writer.writeByte(0);
try data_writer.writeInt(u16, key, .little);
try data_writer.writeInt(u16, cmd_id.asWord(), .little);
try data_writer.writeInt(u16, 0, .little);
}
}
const DialogOptionalStatementValues = struct {
style: u32 = res.WS.SYSMENU | res.WS.BORDER | res.WS.POPUP,
exstyle: u32 = 0,
class: ?NameOrOrdinal = null,
menu: ?NameOrOrdinal = null,
font: ?FontStatementValues = null,
caption: ?Token = null,
};
pub fn writeDialog(self: *Compiler, node: *Node.Dialog, writer: *std.Io.Writer) !void {
var data_buffer: std.Io.Writer.Allocating = .init(self.allocator);
defer data_buffer.deinit();
const resource = ResourceType.fromString(.{
.slice = node.type.slice(self.source),
.code_page = self.input_code_pages.getForToken(node.type),
});
std.debug.assert(resource == .dialog or resource == .dialogex);
var optional_statement_values: DialogOptionalStatementValues = .{};
defer {
if (optional_statement_values.class) |class| {
class.deinit(self.allocator);
}
if (optional_statement_values.menu) |menu| {
menu.deinit(self.allocator);
}
}
var last_menu: *Node.SimpleStatement = undefined;
var last_class: *Node.SimpleStatement = undefined;
var last_menu_would_be_forced_ordinal = false;
var last_menu_has_digit_as_first_char = false;
var last_menu_did_uppercase = false;
var last_class_would_be_forced_ordinal = false;
for (node.optional_statements) |optional_statement| {
switch (optional_statement.id) {
.simple_statement => {
const simple_statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", optional_statement));
const statement_identifier = simple_statement.identifier;
const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
switch (statement_type) {
.style, .exstyle => {
const style = evaluateFlagsExpressionWithDefault(0, simple_statement.value, self.source, self.input_code_pages);
if (statement_type == .style) {
optional_statement_values.style = style;
} else {
optional_statement_values.exstyle = style;
}
},
.caption => {
std.debug.assert(simple_statement.value.id == .literal);
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", simple_statement.value));
optional_statement_values.caption = literal_node.token;
},
.class => {
const is_duplicate = optional_statement_values.class != null;
const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;
// an ordinal exist, it affects all future CLASS statements and forces
// them to be treated as an ordinal no matter what.
if (forced_ordinal) {
last_class_would_be_forced_ordinal = true;
}
if (optional_statement_values.class) |prev| {
prev.deinit(self.allocator);
optional_statement_values.class = null;
}
if (simple_statement.value.isNumberExpression()) {
const class_ordinal = evaluateNumberExpression(simple_statement.value, self.source, self.input_code_pages);
optional_statement_values.class = NameOrOrdinal{ .ordinal = class_ordinal.asWord() };
} else {
std.debug.assert(simple_statement.value.isStringLiteral());
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", simple_statement.value));
const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
optional_statement_values.class = NameOrOrdinal{ .name = parsed };
}
last_class = simple_statement;
},
.menu => {
const is_duplicate = optional_statement_values.menu != null;
const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;
// an ordinal exist, it affects all future MENU statements and forces
// them to be treated as an ordinal no matter what.
if (forced_ordinal) {
last_menu_would_be_forced_ordinal = true;
}
if (optional_statement_values.menu) |prev| {
prev.deinit(self.allocator);
optional_statement_values.menu = null;
}
std.debug.assert(simple_statement.value.id == .literal);
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", simple_statement.value));
const token_slice = literal_node.token.slice(self.source);
const bytes = SourceBytes{
.slice = token_slice,
.code_page = self.input_code_pages.getForToken(literal_node.token),
};
optional_statement_values.menu = try NameOrOrdinal.fromString(self.allocator, bytes);
if (optional_statement_values.menu.? == .name) {
if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(bytes)) |win32_rc_ordinal| {
try self.addErrorDetails(.{
.err = .invalid_digit_character_in_ordinal,
.type = .err,
.token = literal_node.token,
});
return self.addErrorDetailsAndFail(.{
.err = .win32_non_ascii_ordinal,
.type = .note,
.token = literal_node.token,
.print_source_line = false,
.extra = .{ .number = win32_rc_ordinal.ordinal },
});
}
}
// in order to emit the appropriate warning(s) later on.
// See where the warning are emitted below (outside this loop)
// for the full explanation.
var did_uppercase = false;
var codepoint_i: usize = 0;
while (bytes.code_page.codepointAt(codepoint_i, bytes.slice)) |codepoint| : (codepoint_i += codepoint.byte_len) {
const c = codepoint.value;
switch (c) {
'a'...'z' => {
did_uppercase = true;
break;
},
else => {},
}
}
last_menu_did_uppercase = did_uppercase;
last_menu_has_digit_as_first_char = std.ascii.isDigit(token_slice[0]);
last_menu = simple_statement;
},
else => {},
}
},
.font_statement => {
const font: *Node.FontStatement = @alignCast(@fieldParentPtr("base", optional_statement));
if (optional_statement_values.font != null) {
optional_statement_values.font.?.node = font;
} else {
optional_statement_values.font = FontStatementValues{ .node = font };
}
if (font.weight) |weight| {
const value = evaluateNumberExpression(weight, self.source, self.input_code_pages);
optional_statement_values.font.?.weight = value.asWord();
}
if (font.italic) |italic| {
const value = evaluateNumberExpression(italic, self.source, self.input_code_pages);
optional_statement_values.font.?.italic = value.asWord() != 0;
}
},
else => {},
}
}
// Multiple CLASS parameters are specified and any of them are treated as a number, then
// the last CLASS is always treated as a number no matter what
if (last_class_would_be_forced_ordinal and optional_statement_values.class.? == .name) {
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", last_class.value));
const ordinal_value = res.ForcedOrdinal.fromUtf16Le(optional_statement_values.class.?.name);
try self.addErrorDetails(.{
.err = .rc_would_miscompile_dialog_class,
.type = .warning,
.token = literal_node.token,
.extra = .{ .number = ordinal_value },
});
try self.addErrorDetails(.{
.err = .rc_would_miscompile_dialog_class,
.type = .note,
.print_source_line = false,
.token = literal_node.token,
.extra = .{ .number = ordinal_value },
});
try self.addErrorDetails(.{
.err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
.type = .note,
.print_source_line = false,
.token = literal_node.token,
.extra = .{ .menu_or_class = .class },
});
}
// 1. The first character of the ID is a digit, in which case it is always treated as a number
// no matter what (and therefore does not match how the MENU/MENUEX id is parsed)
// 2. Multiple MENU parameters are specified and any of them are treated as a number, then
// the last MENU is always treated as a number no matter what
if ((last_menu_would_be_forced_ordinal or last_menu_has_digit_as_first_char) and optional_statement_values.menu.? == .name) {
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", last_menu.value));
const token_slice = literal_node.token.slice(self.source);
const bytes = SourceBytes{
.slice = token_slice,
.code_page = self.input_code_pages.getForToken(literal_node.token),
};
const ordinal_value = res.ForcedOrdinal.fromBytes(bytes);
try self.addErrorDetails(.{
.err = .rc_would_miscompile_dialog_menu_id,
.type = .warning,
.token = literal_node.token,
.extra = .{ .number = ordinal_value },
});
try self.addErrorDetails(.{
.err = .rc_would_miscompile_dialog_menu_id,
.type = .note,
.print_source_line = false,
.token = literal_node.token,
.extra = .{ .number = ordinal_value },
});
if (last_menu_would_be_forced_ordinal) {
try self.addErrorDetails(.{
.err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
.type = .note,
.print_source_line = false,
.token = literal_node.token,
.extra = .{ .menu_or_class = .menu },
});
} else {
try self.addErrorDetails(.{
.err = .rc_would_miscompile_dialog_menu_id_starts_with_digit,
.type = .note,
.print_source_line = false,
.token = literal_node.token,
});
}
}
// which means that it will convert ASCII characters to uppercase during the 'name' parsing.
// This turns out not to matter (`LoadMenu` does a case-insensitive lookup anyway),
// but it still makes sense to share the uppercasing logic since the MENU parameter
// here is just a reference to a MENU/MENUEX id within the .exe.
// So, because this is an intentional but inconsequential-to-the-user difference
// between resinator and the Win32 RC compiler, we only emit a hint instead of
// a warning.
if (last_menu_did_uppercase) {
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", last_menu.value));
try self.addErrorDetails(.{
.err = .dialog_menu_id_was_uppercased,
.type = .hint,
.token = literal_node.token,
});
}
const x = evaluateNumberExpression(node.x, self.source, self.input_code_pages);
const y = evaluateNumberExpression(node.y, self.source, self.input_code_pages);
const width = evaluateNumberExpression(node.width, self.source, self.input_code_pages);
const height = evaluateNumberExpression(node.height, self.source, self.input_code_pages);
if (optional_statement_values.font) |_| {
optional_statement_values.style |= res.DS.SETFONT;
} else {
optional_statement_values.style &= ~res.DS.SETFONT;
}
if (optional_statement_values.caption) |_| {
optional_statement_values.style |= res.WS.CAPTION;
}
// on their own.
try self.writeDialogHeaderAndStrings(
node,
&data_buffer.writer,
resource,
&optional_statement_values,
x,
y,
width,
height,
);
var controls_by_id = std.AutoHashMap(u32, *const Node.ControlStatement).init(self.allocator);
try controls_by_id.ensureTotalCapacity(@as(u16, @intCast(node.controls.len)));
defer controls_by_id.deinit();
for (node.controls) |control_node| {
const control: *Node.ControlStatement = @alignCast(@fieldParentPtr("base", control_node));
try self.writeDialogControl(
control,
&data_buffer.writer,
resource,
@intCast(data_buffer.written().len),
&controls_by_id,
);
if (data_buffer.written().len > std.math.maxInt(u32)) {
try self.addErrorDetails(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.type = .note,
.token = control.type,
});
}
}
const data_size: u32 = @intCast(data_buffer.written().len);
var header = try self.resourceHeader(node.id, node.type, .{
.data_size = data_size,
});
defer header.deinit(self.allocator);
header.applyMemoryFlags(node.common_resource_attributes, self.source);
header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
try header.write(writer, self.errContext(node.id));
var data_fbs: std.Io.Reader = .fixed(data_buffer.written());
try writeResourceData(writer, &data_fbs, data_size);
}
fn writeDialogHeaderAndStrings(
self: *Compiler,
node: *Node.Dialog,
data_writer: *std.Io.Writer,
resource: ResourceType,
optional_statement_values: *const DialogOptionalStatementValues,
x: Number,
y: Number,
width: Number,
height: Number,
) !void {
if (resource == .dialogex) {
const help_id: u32 = help_id: {
if (node.help_id == null) break :help_id 0;
break :help_id evaluateNumberExpression(node.help_id.?, self.source, self.input_code_pages).value;
};
try data_writer.writeInt(u16, 1, .little);
try data_writer.writeInt(u16, 0xFFFF, .little);
try data_writer.writeInt(u32, help_id, .little);
try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
try data_writer.writeInt(u32, optional_statement_values.style, .little);
} else {
try data_writer.writeInt(u32, optional_statement_values.style, .little);
try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
}
// is within the range of a u16.
try data_writer.writeInt(u16, @as(u16, @intCast(node.controls.len)), .little);
try data_writer.writeInt(u16, x.asWord(), .little);
try data_writer.writeInt(u16, y.asWord(), .little);
try data_writer.writeInt(u16, width.asWord(), .little);
try data_writer.writeInt(u16, height.asWord(), .little);
if (optional_statement_values.menu) |menu| {
try menu.write(data_writer);
} else {
try data_writer.writeInt(u16, 0, .little);
}
if (optional_statement_values.class) |class| {
try class.write(data_writer);
} else {
try data_writer.writeInt(u16, 0, .little);
}
if (optional_statement_values.caption) |caption| {
const parsed = try self.parseQuotedStringAsWideString(caption);
defer self.allocator.free(parsed);
try data_writer.writeAll(std.mem.sliceAsBytes(parsed[0 .. parsed.len + 1]));
} else {
try data_writer.writeInt(u16, 0, .little);
}
if (optional_statement_values.font) |font| {
try self.writeDialogFont(resource, font, data_writer);
}
}
fn writeDialogControl(
self: *Compiler,
control: *Node.ControlStatement,
data_writer: *std.Io.Writer,
resource: ResourceType,
bytes_written_so_far: u32,
controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),
) !void {
const control_type = rc.Control.map.get(control.type.slice(self.source)).?;
// compiler will miscompile controls if their extra data ends on an odd offset.
// We will avoid the miscompilation and emit a warning.
const num_padding = numPaddingBytesNeeded(bytes_written_so_far);
if (num_padding == 1 or num_padding == 3) {
try self.addErrorDetails(.{
.err = .rc_would_miscompile_control_padding,
.type = .warning,
.token = control.type,
});
try self.addErrorDetails(.{
.err = .rc_would_miscompile_control_padding,
.type = .note,
.print_source_line = false,
.token = control.type,
});
}
try data_writer.splatByteAll(0, num_padding);
const style = if (control.style) |style_expression|
evaluateFlagsExpressionWithDefault(res.ControlClass.getImpliedStyle(control_type), style_expression, self.source, self.input_code_pages)
else
res.ControlClass.getImpliedStyle(control_type);
const exstyle = if (control.exstyle) |exstyle_expression|
evaluateFlagsExpressionWithDefault(0, exstyle_expression, self.source, self.input_code_pages)
else
0;
switch (resource) {
.dialog => {
try data_writer.writeInt(u32, style, .little);
try data_writer.writeInt(u32, exstyle, .little);
},
.dialogex => {
const help_id: u32 = if (control.help_id) |help_id_expression|
evaluateNumberExpression(help_id_expression, self.source, self.input_code_pages).value
else
0;
try data_writer.writeInt(u32, help_id, .little);
try data_writer.writeInt(u32, exstyle, .little);
try data_writer.writeInt(u32, style, .little);
},
else => unreachable,
}
const control_x = evaluateNumberExpression(control.x, self.source, self.input_code_pages);
const control_y = evaluateNumberExpression(control.y, self.source, self.input_code_pages);
const control_width = evaluateNumberExpression(control.width, self.source, self.input_code_pages);
const control_height = evaluateNumberExpression(control.height, self.source, self.input_code_pages);
try data_writer.writeInt(u16, control_x.asWord(), .little);
try data_writer.writeInt(u16, control_y.asWord(), .little);
try data_writer.writeInt(u16, control_width.asWord(), .little);
try data_writer.writeInt(u16, control_height.asWord(), .little);
const control_id = evaluateNumberExpression(control.id, self.source, self.input_code_pages);
switch (resource) {
.dialog => try data_writer.writeInt(u16, control_id.asWord(), .little),
.dialogex => try data_writer.writeInt(u32, control_id.value, .little),
else => unreachable,
}
const control_id_for_map: u32 = switch (resource) {
.dialog => control_id.asWord(),
.dialogex => control_id.value,
else => unreachable,
};
const result = controls_by_id.getOrPutAssumeCapacity(control_id_for_map);
if (result.found_existing) {
if (!self.silent_duplicate_control_ids) {
try self.addErrorDetails(.{
.err = .control_id_already_defined,
.type = .warning,
.token = control.id.getFirstToken(),
.token_span_end = control.id.getLastToken(),
.extra = .{ .number = control_id_for_map },
});
try self.addErrorDetails(.{
.err = .control_id_already_defined,
.type = .note,
.token = result.value_ptr.*.id.getFirstToken(),
.token_span_end = result.value_ptr.*.id.getLastToken(),
.extra = .{ .number = control_id_for_map },
});
}
} else {
result.value_ptr.* = control;
}
if (res.ControlClass.fromControl(control_type)) |control_class| {
const ordinal = NameOrOrdinal{ .ordinal = @backingInt(control_class) };
try ordinal.write(data_writer);
} else {
const class_node = control.class.?;
if (class_node.isNumberExpression()) {
const number = evaluateNumberExpression(class_node, self.source, self.input_code_pages);
const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
// but I think that's a miscompilation/bug of the Windows implementation.
// The Windows behavior is (where LSB = least significant byte):
// - If the LSB is 0x00 => 0xFFFF0000
// - If the LSB is < 0x80 => 0x000000<LSB>
// - If the LSB is >= 0x80 => 0x0000FF<LSB>
//
// Because of this, we emit a warning about the potential miscompilation
try self.addErrorDetails(.{
.err = .rc_would_miscompile_control_class_ordinal,
.type = .warning,
.token = class_node.getFirstToken(),
.token_span_end = class_node.getLastToken(),
});
try self.addErrorDetails(.{
.err = .rc_would_miscompile_control_class_ordinal,
.type = .note,
.print_source_line = false,
.token = class_node.getFirstToken(),
.token_span_end = class_node.getLastToken(),
});
try ordinal.write(data_writer);
} else if (class_node.isStringLiteral()) {
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", class_node));
const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
defer self.allocator.free(parsed);
if (rc.ControlClass.fromWideString(parsed)) |control_class| {
const ordinal = NameOrOrdinal{ .ordinal = @backingInt(control_class) };
try ordinal.write(data_writer);
} else {
// TODO: Maybe warn when parsed_terminated.len != parsed.len, since
// it seems unlikely that NUL-termination is something intentional
const parsed_terminated = std.mem.sliceTo(parsed, 0);
const name = NameOrOrdinal{ .name = parsed_terminated };
try name.write(data_writer);
}
} else {
const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", class_node));
const literal_slice = literal_node.token.slice(self.source);
const control_class = rc.ControlClass.map.get(literal_slice) orelse unreachable;
const ordinal = NameOrOrdinal{ .ordinal = @backingInt(control_class) };
try ordinal.write(data_writer);
}
}
if (control.text) |text_token| {
const bytes = SourceBytes{
.slice = text_token.slice(self.source),
.code_page = self.input_code_pages.getForToken(text_token),
};
if (text_token.isStringLiteral()) {
const text = try self.parseQuotedStringAsWideString(text_token);
defer self.allocator.free(text);
const name = NameOrOrdinal{ .name = text };
try name.write(data_writer);
} else {
std.debug.assert(text_token.id == .number);
const number = literals.parseNumberLiteral(bytes);
const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
try ordinal.write(data_writer);
}
} else {
try NameOrOrdinal.writeEmpty(data_writer);
}
var extra_data_buf: std.Io.Writer.Allocating = .init(self.allocator);
defer extra_data_buf.deinit();
for (control.extra_data) |data_expression| {
const data = try self.evaluateDataExpression(data_expression);
defer data.deinit(self.allocator);
try data.write(&extra_data_buf.writer);
if (extra_data_buf.written().len > std.math.maxInt(u16)) {
try self.addErrorDetails(.{
.err = .control_extra_data_size_exceeds_max,
.token = control.type,
});
return self.addErrorDetailsAndFail(.{
.err = .control_extra_data_size_exceeds_max,
.type = .note,
.token = data_expression.getFirstToken(),
.token_span_end = data_expression.getLastToken(),
});
}
}
const extra_data_size: u16 = @intCast(extra_data_buf.written().len);
try data_writer.writeInt(u16, extra_data_size, .little);
try data_writer.writeAll(extra_data_buf.written());
}
pub fn writeToolbar(self: *Compiler, node: *Node.Toolbar, writer: *std.Io.Writer) !void {
var data_buffer: std.Io.Writer.Allocating = .init(self.allocator);
defer data_buffer.deinit();
const data_writer = &data_buffer.writer;
const button_width = evaluateNumberExpression(node.button_width, self.source, self.input_code_pages);
const button_height = evaluateNumberExpression(node.button_height, self.source, self.input_code_pages);
// TODO: Try to find something mentioning this
try data_writer.writeInt(u16, 1, .little);
try data_writer.writeInt(u16, button_width.asWord(), .little);
try data_writer.writeInt(u16, button_height.asWord(), .little);
try data_writer.writeInt(u16, @as(u16, @intCast(node.buttons.len)), .little);
for (node.buttons) |button_or_sep| {
switch (button_or_sep.id) {
.literal => {
std.debug.assert(button_or_sep.cast(.literal).?.token.id == .literal);
try data_writer.writeInt(u16, 0, .little);
},
.simple_statement => {
const value_node = button_or_sep.cast(.simple_statement).?.value;
const value = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
try data_writer.writeInt(u16, value.asWord(), .little);
},
else => unreachable,
}
}
const data_size: u32 = @intCast(data_buffer.written().len);
var header = try self.resourceHeader(node.id, node.type, .{
.data_size = data_size,
});
defer header.deinit(self.allocator);
header.applyMemoryFlags(node.common_resource_attributes, self.source);
try header.write(writer, self.errContext(node.id));
var data_fbs: std.Io.Reader = .fixed(data_buffer.written());
try writeResourceData(writer, &data_fbs, data_size);
}
const FontStatementValues = struct {
weight: u16 = 0,
italic: bool = false,
node: *Node.FontStatement,
};
pub fn writeDialogFont(self: *Compiler, resource: ResourceType, values: FontStatementValues, writer: *std.Io.Writer) !void {
const node = values.node;
const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);
try writer.writeInt(u16, point_size.asWord(), .little);
if (resource == .dialogex) {
try writer.writeInt(u16, values.weight, .little);
}
if (resource == .dialogex) {
try writer.writeInt(u8, @intFromBool(values.italic), .little);
}
if (node.char_set) |char_set| {
const value = evaluateNumberExpression(char_set, self.source, self.input_code_pages);
try writer.writeInt(u8, @as(u8, @truncate(value.value)), .little);
} else if (resource == .dialogex) {
try writer.writeInt(u8, 1, .little);
}
const typeface = try self.parseQuotedStringAsWideString(node.typeface);
defer self.allocator.free(typeface);
try writer.writeAll(std.mem.sliceAsBytes(typeface[0 .. typeface.len + 1]));
}
pub fn writeMenu(self: *Compiler, node: *Node.Menu, writer: *std.Io.Writer) !void {
var data_buffer: std.Io.Writer.Allocating = .init(self.allocator);
defer data_buffer.deinit();
const type_bytes = SourceBytes{
.slice = node.type.slice(self.source),
.code_page = self.input_code_pages.getForToken(node.type),
};
const resource = ResourceType.fromString(type_bytes);
std.debug.assert(resource == .menu or resource == .menuex);
try self.writeMenuData(node, &data_buffer.writer, resource);
const data_size: u32 = std.math.cast(u32, data_buffer.written().len) orelse {
return self.addErrorDetailsAndFail(.{
.err = .resource_data_size_exceeds_max,
.token = node.id,
});
};
var header = try self.resourceHeader(node.id, node.type, .{
.data_size = data_size,
});
defer header.deinit(self.allocator);
header.applyMemoryFlags(node.common_resource_attributes, self.source);
header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
try header.write(writer, self.errContext(node.id));
var data_fbs: std.Io.Reader = .fixed(data_buffer.written());
try writeResourceData(writer, &data_fbs, data_size);
}
pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: *std.Io.Writer, resource: ResourceType) !void {
const version: u16 = if (resource == .menu) 0 else 1;
try data_writer.writeInt(u16, version, .little);
const header_size: u16 = if (resource == .menu) 0 else 4;
try data_writer.writeInt(u16, header_size, .little);
// Note: There can be extra bytes at the end of this header (`rgbExtra`),
// but they are always zero-length for us, so we don't write anything
// (the length of the rgbExtra field is inferred from the header_size).
// MENU => rgbExtra: [cbHeaderSize]u8
// MENUEX => rgbExtra: [cbHeaderSize-4]u8
if (resource == .menuex) {
if (node.help_id) |help_id_node| {
const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
try data_writer.writeInt(u32, help_id.value, .little);
} else {
try data_writer.writeInt(u32, 0, .little);
}
}
for (node.items, 0..) |item, i| {
const is_last = i == node.items.len - 1;
try self.writeMenuItem(item, data_writer, is_last);
}
}
pub fn writeMenuItem(self: *Compiler, node: *Node, writer: *std.Io.Writer, is_last_of_parent: bool) !void {
switch (node.id) {
.menu_item_separator => {
// https://devblogs.microsoft.com/oldnewthing/20080710-00/?p=21673
//
// The 'correct' way is to set the MF_SEPARATOR flag, but the Win32 RC
// compiler still uses this alternate form, so that's what we use too.
var flags = res.MenuItemFlags{};
if (is_last_of_parent) flags.markLast();
try writer.writeInt(u16, flags.value, .little);
try writer.writeInt(u16, 0, .little);
try writer.writeInt(u16, 0, .little);
},
.menu_item => {
const menu_item: *Node.MenuItem = @alignCast(@fieldParentPtr("base", node));
var flags = res.MenuItemFlags{};
for (menu_item.option_list) |option_token| {
const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
flags.apply(option);
}
if (is_last_of_parent) flags.markLast();
try writer.writeInt(u16, flags.value, .little);
var result = evaluateNumberExpression(menu_item.result, self.source, self.input_code_pages);
try writer.writeInt(u16, result.asWord(), .little);
var text = try self.parseQuotedStringAsWideString(menu_item.text);
defer self.allocator.free(text);
try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
},
.popup => {
const popup: *Node.Popup = @alignCast(@fieldParentPtr("base", node));
var flags = res.MenuItemFlags{ .value = res.MF.POPUP };
for (popup.option_list) |option_token| {
const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
flags.apply(option);
}
if (is_last_of_parent) flags.markLast();
try writer.writeInt(u16, flags.value, .little);
var text = try self.parseQuotedStringAsWideString(popup.text);
defer self.allocator.free(text);
try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
for (popup.items, 0..) |item, i| {
const is_last = i == popup.items.len - 1;
try self.writeMenuItem(item, writer, is_last);
}
},
inline .menu_item_ex, .popup_ex => |node_type| {
const menu_item: *node_type.Type() = @alignCast(@fieldParentPtr("base", node));
if (menu_item.type) |flags| {
const value = evaluateNumberExpression(flags, self.source, self.input_code_pages);
try writer.writeInt(u32, value.value, .little);
} else {
try writer.writeInt(u32, 0, .little);
}
if (menu_item.state) |state| {
const value = evaluateNumberExpression(state, self.source, self.input_code_pages);
try writer.writeInt(u32, value.value, .little);
} else {
try writer.writeInt(u32, 0, .little);
}
if (menu_item.id) |id| {
const value = evaluateNumberExpression(id, self.source, self.input_code_pages);
try writer.writeInt(u32, value.value, .little);
} else {
try writer.writeInt(u32, 0, .little);
}
var flags: u16 = 0;
if (is_last_of_parent) flags |= comptime @as(u16, @intCast(res.MF.END));
if (node_type == .popup_ex) flags |= 0x01;
try writer.writeInt(u16, flags, .little);
var text = try self.parseQuotedStringAsWideString(menu_item.text);
defer self.allocator.free(text);
try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
// non-DWORD alignment, so we can just use the byte length of those
// two values to realign to DWORD alignment.
const relevant_bytes = 2 + (text.len + 1) * 2;
try writeDataPadding(writer, @intCast(relevant_bytes));
if (node_type == .popup_ex) {
if (menu_item.help_id) |help_id_node| {
const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
try writer.writeInt(u32, help_id.value, .little);
} else {
try writer.writeInt(u32, 0, .little);
}
for (menu_item.items, 0..) |item, i| {
const is_last = i == menu_item.items.len - 1;
try self.writeMenuItem(item, writer, is_last);
}
}
},
else => unreachable,
}
}
pub fn writeVersionInfo(self: *Compiler, node: *Node.VersionInfo, writer: *std.Io.Writer) !void {
var data_buffer: std.Io.Writer.Allocating = .init(self.allocator);
defer data_buffer.deinit();
const data_writer = &data_buffer.writer;
try data_writer.writeInt(u16, 0, .little);
try data_writer.writeInt(u16, res.FixedFileInfo.byte_len, .little);
try data_writer.writeInt(u16, res.VersionNode.type_binary, .little);
const key_bytes = std.mem.sliceAsBytes(res.FixedFileInfo.key[0 .. res.FixedFileInfo.key.len + 1]);
try data_writer.writeAll(key_bytes);
// of the node is a constant (FixedFileInfo.key). The total number of bytes
// written so far is 38, so we need 2 padding bytes to get back to DWORD alignment
try data_writer.writeInt(u16, 0, .little);
var fixed_file_info = res.FixedFileInfo{};
for (node.fixed_info) |fixed_info| {
switch (fixed_info.id) {
.version_statement => {
const version_statement: *Node.VersionStatement = @alignCast(@fieldParentPtr("base", fixed_info));
const version_type = rc.VersionInfo.map.get(version_statement.type.slice(self.source)).?;
// potential duplicate PRODUCTVERSION/FILEVERSION statements
switch (version_type) {
.file_version => @memset(&fixed_file_info.file_version.parts, 0),
.product_version => @memset(&fixed_file_info.product_version.parts, 0),
else => unreachable,
}
for (version_statement.parts, 0..) |part, i| {
const part_value = evaluateNumberExpression(part, self.source, self.input_code_pages);
if (part_value.is_long) {
try self.addErrorDetails(.{
.err = .rc_would_error_u16_with_l_suffix,
.type = .warning,
.token = part.getFirstToken(),
.token_span_end = part.getLastToken(),
.extra = .{ .statement_with_u16_param = switch (version_type) {
.file_version => .fileversion,
.product_version => .productversion,
else => unreachable,
} },
});
try self.addErrorDetails(.{
.err = .rc_would_error_u16_with_l_suffix,
.print_source_line = false,
.type = .note,
.token = part.getFirstToken(),
.token_span_end = part.getLastToken(),
.extra = .{ .statement_with_u16_param = switch (version_type) {
.file_version => .fileversion,
.product_version => .productversion,
else => unreachable,
} },
});
}
switch (version_type) {
.file_version => {
fixed_file_info.file_version.parts[i] = part_value.asWord();
},
.product_version => {
fixed_file_info.product_version.parts[i] = part_value.asWord();
},
else => unreachable,
}
}
},
.simple_statement => {
const statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", fixed_info));
const statement_type = rc.VersionInfo.map.get(statement.identifier.slice(self.source)).?;
const value = evaluateNumberExpression(statement.value, self.source, self.input_code_pages);
switch (statement_type) {
.file_flags_mask => fixed_file_info.file_flags_mask = value.value,
.file_flags => fixed_file_info.file_flags = value.value,
.file_os => fixed_file_info.file_os = value.value,
.file_type => fixed_file_info.file_type = value.value,
.file_subtype => fixed_file_info.file_subtype = value.value,
else => unreachable,
}
},
else => unreachable,
}
}
try fixed_file_info.write(data_writer);
for (node.block_statements) |statement| {
var overflow = false;
self.writeVersionNode(statement, data_writer) catch |err| switch (err) {
error.NoSpaceLeft => {
overflow = true;
},
else => |e| return e,
};
if (overflow or data_buffer.written().len > std.math.maxInt(u16)) {
try self.addErrorDetails(.{
.err = .version_node_size_exceeds_max,
.token = node.id,
});
return self.addErrorDetailsAndFail(.{
.err = .version_node_size_exceeds_max,
.type = .note,
.token = statement.getFirstToken(),
.token_span_end = statement.getLastToken(),
});
}
}
// statements loop above which is the only place it can overflow.
const data_size: u16 = @intCast(data_buffer.written().len);
std.mem.writeInt(u16, data_buffer.written()[0..2], data_size, .little);
var header = try self.resourceHeader(node.id, node.versioninfo, .{
.data_size = data_size,
});
defer header.deinit(self.allocator);
header.applyMemoryFlags(node.common_resource_attributes, self.source);
try header.write(writer, self.errContext(node.id));
var data_fbs: std.Io.Reader = .fixed(data_buffer.written());
try writeResourceData(writer, &data_fbs, data_size);
}
pub fn writeVersionNode(self: *Compiler, node: *Node, writer: *std.Io.Writer) !void {
try writeDataPadding(writer, std.math.cast(u16, writer.buffered().len) orelse return error.NoSpaceLeft);
const node_and_children_size_offset = writer.buffered().len;
try writer.writeInt(u16, 0, .little);
const data_size_offset = writer.buffered().len;
try writer.writeInt(u16, 0, .little);
const data_type_offset = writer.buffered().len;
try writer.writeInt(u16, res.VersionNode.type_string, .little);
switch (node.id) {
inline .block, .block_value => |node_type| {
const block_or_value: *node_type.Type() = @alignCast(@fieldParentPtr("base", node));
const parsed_key = try self.parseQuotedStringAsWideString(block_or_value.key);
defer self.allocator.free(parsed_key);
const parsed_key_to_first_null = std.mem.sliceTo(parsed_key, 0);
try writer.writeAll(std.mem.sliceAsBytes(parsed_key_to_first_null[0 .. parsed_key_to_first_null.len + 1]));
var has_number_value: bool = false;
for (block_or_value.values) |value_value_node_uncasted| {
const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
if (value_value_node.expression.isNumberExpression()) {
has_number_value = true;
break;
}
}
// this is a byte count. If there are only strings, then this is a count of
// UTF-16 code units.
//
// The Win32 RC compiler miscompiles this count in the case of values that
// have a mix of numbers and strings. This is detected and a warning is emitted
// during parsing, so we can just do the correct thing here.
var values_size: usize = 0;
try writeDataPadding(writer, std.math.cast(u16, writer.buffered().len) orelse return error.NoSpaceLeft);
for (block_or_value.values, 0..) |value_value_node_uncasted, i| {
const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
const value_node = value_value_node.expression;
if (value_node.isNumberExpression()) {
const number = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
const data_wrapper = Data{ .number = number };
try data_wrapper.write(writer);
values_size += if (number.is_long) 4 else 2;
} else {
std.debug.assert(value_node.isStringLiteral());
const literal_node = value_node.cast(.literal).?;
const parsed_value = try self.parseQuotedStringAsWideString(literal_node.token);
defer self.allocator.free(parsed_value);
const parsed_to_first_null = std.mem.sliceTo(parsed_value, 0);
try writer.writeAll(std.mem.sliceAsBytes(parsed_to_first_null));
// only if there are no number values in the list.
var value_size = parsed_to_first_null.len;
if (has_number_value) value_size *= 2;
values_size += value_size;
// or this is the last value. If the value evaluates to empty, then
// it never gets a null terminator. If there was an explicit null-terminator
// in the string, we still need to potentially add one since we already
// sliced to the terminator.
const is_last = i == block_or_value.values.len - 1;
const is_empty = parsed_to_first_null.len == 0;
const is_only = block_or_value.values.len == 1;
if ((!is_empty or !is_only) and (is_last or value_value_node.trailing_comma)) {
try writer.writeInt(u16, 0, .little);
values_size += if (has_number_value) 2 else 1;
}
}
}
var data_size_slice = writer.buffered()[data_size_offset..];
std.mem.writeInt(u16, data_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(values_size)), .little);
if (has_number_value) {
const data_type_slice = writer.buffered()[data_type_offset..];
std.mem.writeInt(u16, data_type_slice[0..@sizeOf(u16)], res.VersionNode.type_binary, .little);
}
if (node_type == .block) {
const block = block_or_value;
for (block.children) |child| {
try self.writeVersionNode(child, writer);
}
}
},
else => unreachable,
}
const node_and_children_size = writer.buffered().len - node_and_children_size_offset;
const node_and_children_size_slice = writer.buffered()[node_and_children_size_offset..];
std.mem.writeInt(u16, node_and_children_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(node_and_children_size)), .little);
}
pub fn writeStringTable(self: *Compiler, node: *Node.StringTable) !void {
const language = getLanguageFromOptionalStatements(node.optional_statements, self.source, self.input_code_pages) orelse self.state.language;
for (node.strings) |string_node| {
const string: *Node.StringTableString = @alignCast(@fieldParentPtr("base", string_node));
const string_id_data = try self.evaluateDataExpression(string.id);
const string_id = string_id_data.number.asWord();
self.state.string_tables.set(
self.arena,
language,
string_id,
string.string,
&node.base,
self.source,
self.input_code_pages,
self.state.version,
self.state.characteristics,
) catch |err| switch (err) {
error.StringAlreadyDefined => {
// string tokens, but that would mean storing the id token of each string
// which doesn't seem worth it just for slightly better error messages.
try self.addErrorDetails(.{
.err = .string_already_defined,
.token = string.string,
.extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
});
const existing_def_table = self.state.string_tables.tables.getPtr(language).?;
const existing_definition = existing_def_table.get(string_id).?;
return self.addErrorDetailsAndFail(.{
.err = .string_already_defined,
.type = .note,
.token = existing_definition,
.extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
});
},
error.OutOfMemory => |e| return e,
};
}
}
pub fn writeLanguageStatement(self: *Compiler, node: *Node.LanguageStatement) void {
const primary = Compiler.evaluateNumberExpression(node.primary_language_id, self.source, self.input_code_pages);
const sublanguage = Compiler.evaluateNumberExpression(node.sublanguage_id, self.source, self.input_code_pages);
self.state.language.primary_language_id = @truncate(primary.value);
self.state.language.sublanguage_id = @truncate(sublanguage.value);
}
pub fn writeTopLevelSimpleStatement(self: *Compiler, node: *Node.SimpleStatement) void {
const value = Compiler.evaluateNumberExpression(node.value, self.source, self.input_code_pages);
const statement_type = rc.TopLevelKeywords.map.get(node.identifier.slice(self.source)).?;
switch (statement_type) {
.characteristics => self.state.characteristics = value.value,
.version => self.state.version = value.value,
else => unreachable,
}
}
pub const ResourceHeaderOptions = struct {
language: ?res.Language = null,
data_size: DWORD = 0,
};
pub fn resourceHeader(self: *Compiler, id_token: Token, type_token: Token, options: ResourceHeaderOptions) !ResourceHeader {
const id_bytes = self.sourceBytesForToken(id_token);
const type_bytes = self.sourceBytesForToken(type_token);
return ResourceHeader.init(
self.allocator,
id_bytes,
type_bytes,
options.data_size,
options.language orelse self.state.language,
self.state.version,
self.state.characteristics,
) catch |err| switch (err) {
error.OutOfMemory => |e| return e,
error.TypeNonAsciiOrdinal => {
const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes).?;
try self.addErrorDetails(.{
.err = .invalid_digit_character_in_ordinal,
.type = .err,
.token = type_token,
});
return self.addErrorDetailsAndFail(.{
.err = .win32_non_ascii_ordinal,
.type = .note,
.token = type_token,
.print_source_line = false,
.extra = .{ .number = win32_rc_ordinal.ordinal },
});
},
error.IdNonAsciiOrdinal => {
const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes).?;
try self.addErrorDetails(.{
.err = .invalid_digit_character_in_ordinal,
.type = .err,
.token = id_token,
});
return self.addErrorDetailsAndFail(.{
.err = .win32_non_ascii_ordinal,
.type = .note,
.token = id_token,
.print_source_line = false,
.extra = .{ .number = win32_rc_ordinal.ordinal },
});
},
};
}
pub const ResourceHeader = struct {
name_value: NameOrOrdinal,
type_value: NameOrOrdinal,
language: res.Language,
memory_flags: MemoryFlags,
data_size: DWORD,
version: DWORD,
characteristics: DWORD,
data_version: DWORD = 0,
pub const InitError = error{ OutOfMemory, IdNonAsciiOrdinal, TypeNonAsciiOrdinal };
pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {
const type_value = type: {
const resource_type = ResourceType.fromString(type_bytes);
if (res.RT.fromResource(resource_type)) |rt_constant| {
break :type NameOrOrdinal{ .ordinal = @backingInt(rt_constant) };
} else {
break :type try NameOrOrdinal.fromString(allocator, type_bytes);
}
};
errdefer type_value.deinit(allocator);
if (type_value == .name) {
if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes)) |_| {
return error.TypeNonAsciiOrdinal;
}
}
const name_value = try NameOrOrdinal.fromString(allocator, id_bytes);
errdefer name_value.deinit(allocator);
if (name_value == .name) {
if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes)) |_| {
return error.IdNonAsciiOrdinal;
}
}
const predefined_resource_type = type_value.predefinedResourceType();
return ResourceHeader{
.name_value = name_value,
.type_value = type_value,
.data_size = data_size,
.memory_flags = MemoryFlags.defaults(predefined_resource_type),
.language = language,
.version = version,
.characteristics = characteristics,
};
}
pub fn deinit(self: ResourceHeader, allocator: Allocator) void {
self.name_value.deinit(allocator);
self.type_value.deinit(allocator);
}
pub const SizeInfo = struct {
bytes: u32,
padding_after_name: u2,
};
pub fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {
var header_size: u32 = 8;
header_size = try std.math.add(
u32,
header_size,
std.math.cast(u32, self.name_value.byteLen()) orelse return error.Overflow,
);
header_size = try std.math.add(
u32,
header_size,
std.math.cast(u32, self.type_value.byteLen()) orelse return error.Overflow,
);
const padding_after_name = numPaddingBytesNeeded(header_size);
header_size = try std.math.add(u32, header_size, padding_after_name);
header_size = try std.math.add(u32, header_size, 16);
return .{ .bytes = header_size, .padding_after_name = padding_after_name };
}
pub fn writeAssertNoOverflow(self: ResourceHeader, writer: *std.Io.Writer) !void {
return self.writeSizeInfo(writer, self.calcSize() catch unreachable);
}
pub fn write(self: ResourceHeader, writer: *std.Io.Writer, err_ctx: errors.DiagnosticsContext) !void {
const size_info = self.calcSize() catch {
try err_ctx.diagnostics.append(.{
.err = .resource_data_size_exceeds_max,
.code_page = err_ctx.code_page,
.token = err_ctx.token,
});
return error.CompileError;
};
return self.writeSizeInfo(writer, size_info);
}
pub fn writeSizeInfo(self: ResourceHeader, writer: *std.Io.Writer, size_info: SizeInfo) !void {
try writer.writeInt(DWORD, self.data_size, .little);
try writer.writeInt(DWORD, size_info.bytes, .little);
try self.type_value.write(writer);
try self.name_value.write(writer);
try writer.splatByteAll(0, size_info.padding_after_name);
try writer.writeInt(DWORD, self.data_version, .little);
try writer.writeInt(WORD, self.memory_flags.value, .little);
try writer.writeInt(WORD, self.language.asInt(), .little);
try writer.writeInt(DWORD, self.version, .little);
try writer.writeInt(DWORD, self.characteristics, .little);
}
pub fn predefinedResourceType(self: ResourceHeader) ?res.RT {
return self.type_value.predefinedResourceType();
}
pub fn applyMemoryFlags(self: *ResourceHeader, tokens: []Token, source: []const u8) void {
applyToMemoryFlags(&self.memory_flags, tokens, source);
}
pub fn applyOptionalStatements(self: *ResourceHeader, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
applyToOptionalStatements(&self.language, &self.version, &self.characteristics, statements, source, code_page_lookup);
}
};
fn applyToMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
for (tokens) |token| {
const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
flags.set(attribute);
}
}
fn applyToGroupMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
// flags as the Win32 RC compiler for all 986,410 K-permutations of memory flags
// for an ICON resource.
//
// This was arrived at by iterating over the permutations and creating a
// list where each line looks something like this:
// MOVEABLE PRELOAD -> 0x1050 (MOVEABLE|PRELOAD|DISCARDABLE)
//
// and then noticing a few things:
// 1. Any permutation that does not have PRELOAD in it just uses the
// default flags.
const initial_flags = flags.*;
var flags_set: std.enums.EnumSet(rc.CommonResourceAttributes) = .empty;
for (tokens) |token| {
const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
flags_set.insert(attribute);
}
if (!flags_set.contains(.preload)) return;
// results in no actual change by the end will just use the default flags.
// For example, `PRELOAD LOADONCALL` will result in default flags, but
// `LOADONCALL PRELOAD` will have PRELOAD set after they are both applied in order.
for (tokens) |token| {
const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
switch (attribute) {
.preload, .loadoncall => flags.set(attribute),
else => {},
}
}
if (flags.value == initial_flags.value) return;
// implies `flags &= ~SHARED` and LOADONCALL implies `flags |= SHARED`
const shared_set = comptime blk: {
var set: std.enums.EnumSet(rc.CommonResourceAttributes) = .empty;
set.insert(.discardable);
set.insert(.shared);
set.insert(.pure);
break :blk set;
};
const discardable_shared_or_pure_specified = flags_set.intersectWith(shared_set).count() != 0;
for (tokens) |token| {
const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
flags.setGroup(attribute, !discardable_shared_or_pure_specified);
}
}
fn applyToOptionalStatements(language: *res.Language, version: *u32, characteristics: *u32, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
for (statements) |node| switch (node.id) {
.language_statement => {
const language_statement: *Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
language.* = languageFromLanguageStatement(language_statement, source, code_page_lookup);
},
.simple_statement => {
const simple_statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node));
const statement_type = rc.OptionalStatements.map.get(simple_statement.identifier.slice(source)) orelse continue;
const result = Compiler.evaluateNumberExpression(simple_statement.value, source, code_page_lookup);
switch (statement_type) {
.version => version.* = result.value,
.characteristics => characteristics.* = result.value,
else => unreachable,
}
},
else => {},
};
}
pub fn languageFromLanguageStatement(language_statement: *const Node.LanguageStatement, source: []const u8, code_page_lookup: *const CodePageLookup) res.Language {
const primary = Compiler.evaluateNumberExpression(language_statement.primary_language_id, source, code_page_lookup);
const sublanguage = Compiler.evaluateNumberExpression(language_statement.sublanguage_id, source, code_page_lookup);
return .{
.primary_language_id = @truncate(primary.value),
.sublanguage_id = @truncate(sublanguage.value),
};
}
pub fn getLanguageFromOptionalStatements(statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) ?res.Language {
for (statements) |node| switch (node.id) {
.language_statement => {
const language_statement: *Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
return languageFromLanguageStatement(language_statement, source, code_page_lookup);
},
else => continue,
};
return null;
}
pub fn writeEmptyResource(writer: *std.Io.Writer) !void {
const header = ResourceHeader{
.name_value = .{ .ordinal = 0 },
.type_value = .{ .ordinal = 0 },
.language = .{
.primary_language_id = 0,
.sublanguage_id = 0,
},
.memory_flags = .{ .value = 0 },
.data_size = 0,
.version = 0,
.characteristics = 0,
};
try header.writeAssertNoOverflow(writer);
}
pub fn sourceBytesForToken(self: *Compiler, token: Token) SourceBytes {
return .{
.slice = token.slice(self.source),
.code_page = self.input_code_pages.getForToken(token),
};
}
pub fn parseQuotedStringAsWideString(self: *Compiler, token: Token) ![:0]u16 {
return literals.parseQuotedStringAsWideString(
self.allocator,
self.sourceBytesForToken(token),
.{
.start_column = token.calculateColumn(self.source, 8, null),
.diagnostics = self.errContext(token),
.output_code_page = self.output_code_pages.getForToken(token),
},
);
}
fn addErrorDetailsWithCodePage(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
try self.diagnostics.append(details);
}
fn addErrorDetails(self: *Compiler, details_without_code_page: errors.ErrorDetailsWithoutCodePage) Allocator.Error!void {
const details = ErrorDetails{
.err = details_without_code_page.err,
.code_page = self.input_code_pages.getForToken(details_without_code_page.token),
.token = details_without_code_page.token,
.token_span_start = details_without_code_page.token_span_start,
.token_span_end = details_without_code_page.token_span_end,
.type = details_without_code_page.type,
.print_source_line = details_without_code_page.print_source_line,
.extra = details_without_code_page.extra,
};
try self.addErrorDetailsWithCodePage(details);
}
fn addErrorDetailsAndFail(self: *Compiler, details_without_code_page: errors.ErrorDetailsWithoutCodePage) error{ CompileError, OutOfMemory } {
try self.addErrorDetails(details_without_code_page);
return error.CompileError;
}
fn errContext(self: *Compiler, token: Token) errors.DiagnosticsContext {
return .{
.diagnostics = self.diagnostics,
.token = token,
.code_page = self.input_code_pages.getForToken(token),
};
}
}