diff options
| author | MrDmitry <aidenhaledev@gmail.com> | 2024-01-23 00:54:50 -0500 |
|---|---|---|
| committer | MrDmitry <aidenhaledev@gmail.com> | 2024-01-26 13:33:17 -0500 |
| commit | 834f8d45babe20beb086faaceaea2c0d237f3259 (patch) | |
| tree | d0c7a85c0d731de4e49f10011f9ecdc705091b5e /lib/std/Build/Step/ConfigHeader.zig | |
| parent | 2ce32e44978428c367551c5c150d1ab12c0b73d7 (diff) | |
| download | zig-834f8d45babe20beb086faaceaea2c0d237f3259.tar.gz zig-834f8d45babe20beb086faaceaea2c0d237f3259.zip | |
Rewrite replace_variables with CMake-specific version
Behavior matches CMake's CMP0053 policy that is the current standard for
variable expansion for `configure_file()`
Diffstat (limited to 'lib/std/Build/Step/ConfigHeader.zig')
| -rw-r--r-- | lib/std/Build/Step/ConfigHeader.zig | 328 |
1 files changed, 282 insertions, 46 deletions
diff --git a/lib/std/Build/Step/ConfigHeader.zig b/lib/std/Build/Step/ConfigHeader.zig index 44d387f524..438805508f 100644 --- a/lib/std/Build/Step/ConfigHeader.zig +++ b/lib/std/Build/Step/ConfigHeader.zig @@ -32,6 +32,28 @@ pub const Value = union(enum) { string: []const u8, }; +fn formatValueCMake(data: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + _ = fmt; + _ = options; + + switch (data) { + .undef, .defined => {}, + .boolean => |b| { + try writer.print("{d}", .{@intFromBool(b)}); + }, + .int => |i| { + try writer.print("{d}", .{i}); + }, + .ident, .string => |s| { + try writer.writeAll(s); + }, + } +} + +fn fmtValueCMake(value: Value) std.fmt.Formatter(formatValueCMake) { + return .{ .data = value }; +} + step: Step, values: std.StringArrayHashMap(Value), output_file: std.Build.GeneratedFile, @@ -313,10 +335,18 @@ fn render_cmake( while (line_it.next()) |raw_line| : (line_index += 1) { const last_line = line_it.index == line_it.buffer.len; - const first_pass = replace_variables(allocator, raw_line, values, "@", "@") catch @panic("Failed to substitute"); - const line = replace_variables(allocator, first_pass, values, "${", "}") catch @panic("Failed to substitute"); - - allocator.free(first_pass); + const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) { + error.InvalidCharacter => { + try step.addError("{s}:{d}: error: invalid character in a variable name", .{ + src_path, line_index + 1, + }); + any_errors = true; + continue; + }, + else => { + @panic("Failed to substitute"); + }, + }; defer allocator.free(line); if (!std.mem.startsWith(u8, line, "#")) { @@ -514,64 +544,270 @@ fn renderValueNasm(output: *std.ArrayList(u8), name: []const u8, value: Value) ! } } -fn replace_variables( +fn expand_variables_cmake( allocator: Allocator, contents: []const u8, values: std.StringArrayHashMap(Value), - prefix: []const u8, - suffix: []const u8, ) ![]const u8 { - var content_buf = allocator.dupe(u8, contents) catch @panic("OOM"); + var content_buf = allocator.alloc(u8, 0) catch @panic("OOM"); + errdefer allocator.free(content_buf); - var last_index: usize = 0; - while (std.mem.indexOfPos(u8, content_buf, last_index, prefix)) |prefix_index| { - const start_index = prefix_index + prefix.len; - if (std.mem.indexOfPos(u8, content_buf, start_index, suffix)) |suffix_index| { - const end_index = suffix_index + suffix.len; + const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-"; + const open_var = "${"; - const beginline = content_buf[0..prefix_index]; - const endline = content_buf[end_index..]; - const key = content_buf[start_index..suffix_index]; - const value = values.get(key) orelse .undef; - - switch (value) { - .boolean => |b| { - const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, @intFromBool(b), endline }); - last_index = prefix_index + 1; + var curr: usize = 0; + var source_offset: usize = 0; + const Position = struct { + source: usize, + target: usize, + }; + var var_stack = std.ArrayList(Position).init(allocator); + defer var_stack.deinit(); + loop: while (curr < contents.len) : (curr += 1) { + switch (contents[curr]) { + '@' => blk: { + if (std.mem.indexOfScalarPos(u8, contents, curr + 1, '@')) |close_pos| { + if (close_pos == curr + 1) { + // closed immediately, preserve as a literal + break :blk; + } + const valid_varname_end = std.mem.indexOfNonePos(u8, contents, curr + 1, valid_varname_chars); + if (valid_varname_end == null or valid_varname_end != close_pos) { + // contains invalid characters, preserve as a literal + break :blk; + } + const key = contents[curr + 1 .. close_pos]; + const value = values.get(key) orelse .undef; + const missing = contents[source_offset..curr]; + const buf = try std.fmt.allocPrint(allocator, "{s}{s}{}", .{ content_buf, missing, fmtValueCMake(value) }); allocator.free(content_buf); content_buf = buf; - }, - .int => |i| { - const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, i, endline }); - const isNegative = i < 0; - const digits = (if (0 < i) std.math.log10(@abs(i)) else 0) + 1; - last_index = prefix_index + @intFromBool(isNegative) + digits; - allocator.free(content_buf); - content_buf = buf; - }, - .string, .ident => |x| { - const buf = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ beginline, x, endline }); - last_index = prefix_index + x.len; + curr = close_pos; + source_offset = close_pos + 1; - allocator.free(content_buf); - content_buf = buf; - }, + continue :loop; + } + }, + '$' => blk: { + const next = curr + 1; + if (next == contents.len or contents[next] != '{') { + // no open bracket detected, preserve as a literal + break :blk; + } + const missing = contents[source_offset..curr]; + const buf = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ content_buf, missing, open_var }); + allocator.free(content_buf); + content_buf = buf; + + source_offset = curr + open_var.len; + curr = next; + try var_stack.append(Position{ + .source = curr, + .target = content_buf.len - open_var.len, + }); + + continue :loop; + }, + '}' => blk: { + if (var_stack.items.len == 0) { + // no open bracket, preserve as a literal + break :blk; + } + const open_pos = var_stack.pop(); + if (source_offset == open_pos.source) { + source_offset += open_var.len; + } + const missing = contents[source_offset..curr]; + const key_start = open_pos.target + open_var.len; + const key = try std.fmt.allocPrint(allocator, "{s}{s}", .{ content_buf[key_start..], missing }); + defer allocator.free(key); - else => { - const buf = try std.fmt.allocPrint(allocator, "{s}{s}", .{ beginline, endline }); - last_index = prefix_index; + const value = values.get(key) orelse .undef; + const buf = try std.fmt.allocPrint(allocator, "{s}{}", .{ content_buf[0..open_pos.target], fmtValueCMake(value) }); + allocator.free(content_buf); + content_buf = buf; - allocator.free(content_buf); - content_buf = buf; - }, - } - continue; + source_offset = curr + 1; + + continue :loop; + }, + else => {}, } - last_index = start_index + 1; + if (var_stack.items.len > 0 and std.mem.indexOfScalar(u8, valid_varname_chars, contents[curr]) == null) { + return error.InvalidCharacter; + } + } + + if (source_offset != contents.len) { + const buf = try std.fmt.allocPrint(allocator, "{s}{s}", .{ content_buf, contents[source_offset..] }); + allocator.free(content_buf); + content_buf = buf; } return content_buf; } + +fn testReplaceVariables( + allocator: Allocator, + contents: []const u8, + expected: []const u8, + values: std.StringArrayHashMap(Value), +) !void { + const actual = try expand_variables_cmake(allocator, contents, values); + defer allocator.free(actual); + + try std.testing.expectEqualStrings(expected, actual); +} + +test "expand_variables_cmake simple cases" { + const allocator = std.testing.allocator; + var values = std.StringArrayHashMap(Value).init(allocator); + defer values.deinit(); + + try values.putNoClobber("undef", .undef); + try values.putNoClobber("defined", .defined); + try values.putNoClobber("true", Value{ .boolean = true }); + try values.putNoClobber("false", Value{ .boolean = false }); + try values.putNoClobber("int", Value{ .int = 42 }); + try values.putNoClobber("ident", Value{ .string = "value" }); + try values.putNoClobber("string", Value{ .string = "text" }); + + // empty strings are preserved + try testReplaceVariables(allocator, "", "", values); + try testReplaceVariables(allocator, "", "", values); + + // line with misc content is preserved + try testReplaceVariables(allocator, "no substitution", "no substitution", values); + + // empty ${} wrapper is removed + try testReplaceVariables(allocator, "${}", "", values); + + // empty @ sigils are preserved + try testReplaceVariables(allocator, "@", "@", values); + try testReplaceVariables(allocator, "@@", "@@", values); + try testReplaceVariables(allocator, "@@@", "@@@", values); + try testReplaceVariables(allocator, "@@@@", "@@@@", values); + + // simple substitution + try testReplaceVariables(allocator, "@undef@", "", values); + try testReplaceVariables(allocator, "${undef}", "", values); + try testReplaceVariables(allocator, "@defined@", "", values); + try testReplaceVariables(allocator, "${defined}", "", values); + try testReplaceVariables(allocator, "@true@", "1", values); + try testReplaceVariables(allocator, "${true}", "1", values); + try testReplaceVariables(allocator, "@false@", "0", values); + try testReplaceVariables(allocator, "${false}", "0", values); + try testReplaceVariables(allocator, "@int@", "42", values); + try testReplaceVariables(allocator, "${int}", "42", values); + try testReplaceVariables(allocator, "@ident@", "value", values); + try testReplaceVariables(allocator, "${ident}", "value", values); + try testReplaceVariables(allocator, "@string@", "text", values); + try testReplaceVariables(allocator, "${string}", "text", values); + + // double packed substitution + try testReplaceVariables(allocator, "@string@@string@", "texttext", values); + try testReplaceVariables(allocator, "${string}${string}", "texttext", values); + + // triple packed substitution + try testReplaceVariables(allocator, "@string@@int@@string@", "text42text", values); + try testReplaceVariables(allocator, "@string@${int}@string@", "text42text", values); + try testReplaceVariables(allocator, "${string}@int@${string}", "text42text", values); + try testReplaceVariables(allocator, "${string}${int}${string}", "text42text", values); + + // double separated substitution + try testReplaceVariables(allocator, "@int@.@int@", "42.42", values); + try testReplaceVariables(allocator, "${int}.${int}", "42.42", values); + + // triple separated substitution + try testReplaceVariables(allocator, "@int@.@true@.@int@", "42.1.42", values); + try testReplaceVariables(allocator, "@int@.${true}.@int@", "42.1.42", values); + try testReplaceVariables(allocator, "${int}.@true@.${int}", "42.1.42", values); + try testReplaceVariables(allocator, "${int}.${true}.${int}", "42.1.42", values); + + // misc prefix is preserved + try testReplaceVariables(allocator, "false is @false@", "false is 0", values); + try testReplaceVariables(allocator, "false is ${false}", "false is 0", values); + + // misc suffix is preserved + try testReplaceVariables(allocator, "@true@ is true", "1 is true", values); + try testReplaceVariables(allocator, "${true} is true", "1 is true", values); + + // surrounding content is preserved + try testReplaceVariables(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); + try testReplaceVariables(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values); + + // incomplete key is preserved + try testReplaceVariables(allocator, "@undef", "@undef", values); + try testReplaceVariables(allocator, "${undef", "${undef", values); + try testReplaceVariables(allocator, "{undef}", "{undef}", values); + try testReplaceVariables(allocator, "undef@", "undef@", values); + try testReplaceVariables(allocator, "undef}", "undef}", values); + + // unknown key is removed + try testReplaceVariables(allocator, "@bad@", "", values); + try testReplaceVariables(allocator, "${bad}", "", values); +} + +test "expand_variables_cmake edge cases" { + const allocator = std.testing.allocator; + var values = std.StringArrayHashMap(Value).init(allocator); + defer values.deinit(); + + // special symbols + try values.putNoClobber("at", Value{ .string = "@" }); + try values.putNoClobber("dollar", Value{ .string = "$" }); + try values.putNoClobber("underscore", Value{ .string = "_" }); + + // basic value + try values.putNoClobber("string", Value{ .string = "text" }); + + // proxy case values + try values.putNoClobber("string_proxy", Value{ .string = "string" }); + try values.putNoClobber("string_at", Value{ .string = "@string@" }); + try values.putNoClobber("string_curly", Value{ .string = "{string}" }); + try values.putNoClobber("string_var", Value{ .string = "${string}" }); + + // stack case values + try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" }); + try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" }); + + // @-vars resolved only when they wrap valid characters, otherwise considered literals + try testReplaceVariables(allocator, "@@string@@", "@text@", values); + try testReplaceVariables(allocator, "@${string}@", "@text@", values); + + // @-vars are resolved inside ${}-vars + try testReplaceVariables(allocator, "${@string_proxy@}", "text", values); + + // expanded variables are considered strings after expansion + try testReplaceVariables(allocator, "@string_at@", "@string@", values); + try testReplaceVariables(allocator, "${string_at}", "@string@", values); + try testReplaceVariables(allocator, "$@string_curly@", "${string}", values); + try testReplaceVariables(allocator, "$${string_curly}", "${string}", values); + try testReplaceVariables(allocator, "${string_var}", "${string}", values); + try testReplaceVariables(allocator, "@string_var@", "${string}", values); + try testReplaceVariables(allocator, "${dollar}{${string}}", "${text}", values); + try testReplaceVariables(allocator, "@dollar@{${string}}", "${text}", values); + try testReplaceVariables(allocator, "@dollar@{@string@}", "${text}", values); + + // when expanded variables contain invalid characters, they prevent further expansion + try testReplaceVariables(allocator, "${${string_var}}", "", values); + try testReplaceVariables(allocator, "${@string_var@}", "", values); + + // nested expanded variables are expanded from the inside out + try testReplaceVariables(allocator, "${string${underscore}proxy}", "string", values); + try testReplaceVariables(allocator, "${string@underscore@proxy}", "string", values); + + // nested vars are only expanded when ${} is closed + try testReplaceVariables(allocator, "@nest@underscore@proxy@", "underscore", values); + try testReplaceVariables(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values); + try testReplaceVariables(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "underscore", values); + try testReplaceVariables(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values); + + // invalid characters lead to an error + try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str*ing}", "", values)); + try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str$ing}", "", values)); + try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str@ing}", "", values)); +} |
