Parses and removes #line commands as well as all source code that is within a file with .c or .h extensions.
RC treats files with the .c and .h extensions in a special manner. It assumes that a file with one of these extensions does not contain resources. If a file has the .c or .h file name extension, RC ignores all lines in the file except the preprocessor directives. Therefore, to include a file that contains resources in another resource script, give the file to be included an extension other than .c or .h. from https://learn.microsoft.com/en-us/windows/win32/menurc/preprocessor-directives
Returns a slice of buf with the aforementioned stuff removed as well as a mapping
between the lines and their corresponding lines in their original files.
buf must be at least as long as source
In-place transformation is supported (i.e. source and buf can be the same slice)
If options.initial_filename is provided, that filename is guaranteed to be
within the mappings.files table and root_filename_offset will be set appropriately.
pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) error
pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) error{ OutOfMemory, InvalidLineCommand, LineNumberOverflow }!ParseLineCommandsResult {
var parse_result = ParseLineCommandsResult{
.result = undefined,
.mappings = .{},
};
errdefer parse_result.mappings.deinit(allocator);
var current_mapping: CurrentMapping = .{};
defer current_mapping.filename.deinit(allocator);
if (options.initial_filename) |initial_filename| {
try current_mapping.filename.appendSlice(allocator, initial_filename);
parse_result.mappings.root_filename_offset = try parse_result.mappings.files.put(allocator, initial_filename);
}
// This implementation attempts to be comment and string aware in order
// to avoid errant #line <num> "<filename>" within multiline comments
// leading to problems in the source mapping after comments are removed,
// but it is not a perfect implementation (intentionally).
//
// The current implementation does not handle cases like
// /* foo */ #line ...
// #line ... // foo
// #line ... /* foo ...
// etc
//
// (the first example will not be recognized as a #line command, the second
// and third will error with InvalidLineCommand)
//
// This is fine, though, since #line commands are generated by the
// preprocessor so in normal circumstances they will be well-formed and
// consistent. The only realistic way the imperfect implementation could
// affect a 'real' use-case would be someone taking the output of a
// preprocessor, editing it manually to add comments before/after #line
// commands, and then running it through resinator with /:no-preprocess.
std.debug.assert(buf.len >= source.len);
var result = UncheckedSliceWriter{ .slice = buf };
const State = enum {
line_start,
preprocessor,
non_preprocessor,
forward_slash,
line_comment,
multiline_comment,
multiline_comment_end,
single_quoted,
single_quoted_escape,
double_quoted,
double_quoted_escape,
};
var state: State = .line_start;
var index: usize = 0;
var pending_start: ?usize = null;
var preprocessor_start: usize = 0;
var line_number: usize = 1;
while (index < source.len) : (index += 1) {
var c = source[index];
state: switch (state) {
.line_start => switch (c) {
'#' => {
preprocessor_start = index;
state = .preprocessor;
if (pending_start == null) {
pending_start = index;
}
},
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
pending_start = null;
},
' ', '\t', '\x0b', '\x0c' => {
if (pending_start == null) {
pending_start = index;
}
},
'/' => {
if (!current_mapping.ignore_contents) {
result.writeSlice(source[pending_start orelse index .. index + 1]);
pending_start = null;
}
state = .forward_slash;
},
'\'' => {
if (!current_mapping.ignore_contents) {
result.writeSlice(source[pending_start orelse index .. index + 1]);
pending_start = null;
}
state = .single_quoted;
},
'"' => {
if (!current_mapping.ignore_contents) {
result.writeSlice(source[pending_start orelse index .. index + 1]);
pending_start = null;
}
state = .double_quoted;
},
else => {
state = .non_preprocessor;
if (pending_start != null) {
if (!current_mapping.ignore_contents) {
result.writeSlice(source[pending_start.? .. index + 1]);
}
pending_start = null;
continue;
}
if (!current_mapping.ignore_contents) {
result.write(c);
}
},
},
.forward_slash => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
},
'/' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .line_comment;
},
'*' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .multiline_comment;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .non_preprocessor;
},
},
.line_comment => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
},
},
.multiline_comment => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
pending_start = null;
},
'*' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .multiline_comment_end;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
},
},
.multiline_comment_end => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .multiline_comment;
pending_start = null;
},
'/' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .non_preprocessor;
},
'*' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
// stay in multiline_comment_end state
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .multiline_comment;
},
},
.single_quoted => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
},
'\\' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .single_quoted_escape;
},
'\'' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .non_preprocessor;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
},
},
.single_quoted_escape => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .single_quoted;
},
},
.double_quoted => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
},
'\\' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .double_quoted_escape;
},
'"' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .non_preprocessor;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
},
},
.double_quoted_escape => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .double_quoted;
},
},
.preprocessor => switch (c) {
'\r', '\n' => {
// Now that we have the full line we can decide what to do with it
const preprocessor_str = source[preprocessor_start..index];
if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
try handleLineCommand(allocator, preprocessor_str, ¤t_mapping);
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
} else {
// Backtrack and reparse the line in the non_preprocessor state,
// since it's possible that this line contains a multiline comment
// start, etc.
state = .non_preprocessor;
index = pending_start.?;
pending_start = null;
// TODO: This is a hacky way to implement this, c needs to be
// updated since we're using continue :state here
c = source[index];
// continue to avoid the index += 1 of the while loop
continue :state .non_preprocessor;
}
},
else => {},
},
.non_preprocessor => switch (c) {
'\r', '\n' => {
const is_crlf = formsLineEndingPair(source, c, index + 1);
if (!current_mapping.ignore_contents) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
result.write(c);
if (is_crlf) result.write(source[index + 1]);
line_number += 1;
}
if (is_crlf) index += 1;
state = .line_start;
pending_start = null;
},
'/' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .forward_slash;
},
'\'' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .single_quoted;
},
'"' => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
state = .double_quoted;
},
else => {
if (!current_mapping.ignore_contents) {
result.write(c);
}
},
},
}
} else {
switch (state) {
.line_start => {},
.forward_slash,
.line_comment,
.multiline_comment,
.multiline_comment_end,
.single_quoted,
.single_quoted_escape,
.double_quoted,
.double_quoted_escape,
.non_preprocessor,
=> {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
},
.preprocessor => {
// Now that we have the full line we can decide what to do with it
const preprocessor_str = source[preprocessor_start..index];
if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
try handleLineCommand(allocator, preprocessor_str, ¤t_mapping);
} else {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
if (!current_mapping.ignore_contents) {
result.writeSlice(source[pending_start.?..index]);
}
}
},
}
}
parse_result.result = result.getWritten();
// Remove whitespace from the end of the result. This avoids issues when the
// preprocessor adds a newline to the end of the file, since then the
// post-preprocessed source could have more lines than the corresponding input source and
// the inserted line can't be mapped to any lines in the original file.
// There's no way that whitespace at the end of a file can affect the parsing
// of the RC script so this is okay to do unconditionally.
// TODO: There might be a better way around this
while (parse_result.result.len > 0 and std.ascii.isWhitespace(parse_result.result[parse_result.result.len - 1])) {
parse_result.result.len -= 1;
}
// If there have been no line mappings at all, then we're dealing with an empty file.
// In this case, we want to fake a line mapping just so that we return something
// that is useable in the same way that a non-empty mapping would be.
if (parse_result.mappings.sources.root == null) {
try handleLineEnd(allocator, line_number, &parse_result.mappings, ¤t_mapping);
}
return parse_result;
}