| ... | ... | @@ -7,7 +7,7 @@ pub const Style = union(enum) { |
| 7 | 7 | /// The configure format supported by autotools. It uses `#undef foo` to |
| 8 | 8 | /// mark lines that can be substituted with different values. |
| 9 | 9 | autoconf: std.Build.LazyPath, |
| 10 | | /// The configure format supported by CMake. It uses `@@FOO@@` and |
| 10 | /// The configure format supported by CMake. It uses `@FOO@`, `${}` and |
| 11 | 11 | /// `#cmakedefine` for template substitution. |
| 12 | 12 | cmake: std.Build.LazyPath, |
| 13 | 13 | /// Instead of starting with an input file, start with nothing. |
| ... | ... | @@ -313,10 +313,22 @@ fn render_cmake( |
| 313 | 313 | while (line_it.next()) |raw_line| : (line_index += 1) { |
| 314 | 314 | const last_line = line_it.index == line_it.buffer.len; |
| 315 | 315 | |
| 316 | | const first_pass = replace_variables(allocator, raw_line, values, "@", "@") catch @panic("Failed to substitute"); |
| 317 | | const line = replace_variables(allocator, first_pass, values, "${", "}") catch @panic("Failed to substitute"); |
| 318 | | |
| 319 | | allocator.free(first_pass); |
| 316 | const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) { |
| 317 | error.InvalidCharacter => { |
| 318 | try step.addError("{s}:{d}: error: invalid character in a variable name", .{ |
| 319 | src_path, line_index + 1, |
| 320 | }); |
| 321 | any_errors = true; |
| 322 | continue; |
| 323 | }, |
| 324 | else => { |
| 325 | try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{ |
| 326 | src_path, line_index + 1, @errorName(err), |
| 327 | }); |
| 328 | any_errors = true; |
| 329 | continue; |
| 330 | }, |
| 331 | }; |
| 320 | 332 | defer allocator.free(line); |
| 321 | 333 | |
| 322 | 334 | if (!std.mem.startsWith(u8, line, "#")) { |
| ... | ... | @@ -514,64 +526,311 @@ fn renderValueNasm(output: *std.ArrayList(u8), name: []const u8, value: Value) ! |
| 514 | 526 | } |
| 515 | 527 | } |
| 516 | 528 | |
| 517 | | fn replace_variables( |
| 529 | fn expand_variables_cmake( |
| 518 | 530 | allocator: Allocator, |
| 519 | 531 | contents: []const u8, |
| 520 | 532 | values: std.StringArrayHashMap(Value), |
| 521 | | prefix: []const u8, |
| 522 | | suffix: []const u8, |
| 523 | 533 | ) ![]const u8 { |
| 524 | | var content_buf = allocator.dupe(u8, contents) catch @panic("OOM"); |
| 534 | var result = std.ArrayList(u8).init(allocator); |
| 535 | errdefer result.deinit(); |
| 525 | 536 | |
| 526 | | var last_index: usize = 0; |
| 527 | | while (std.mem.indexOfPos(u8, content_buf, last_index, prefix)) |prefix_index| { |
| 528 | | const start_index = prefix_index + prefix.len; |
| 529 | | if (std.mem.indexOfPos(u8, content_buf, start_index, suffix)) |suffix_index| { |
| 530 | | const end_index = suffix_index + suffix.len; |
| 537 | const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-"; |
| 538 | const open_var = "${"; |
| 531 | 539 | |
| 532 | | const beginline = content_buf[0..prefix_index]; |
| 533 | | const endline = content_buf[end_index..]; |
| 534 | | const key = content_buf[start_index..suffix_index]; |
| 535 | | const value = values.get(key) orelse .undef; |
| 540 | var curr: usize = 0; |
| 541 | var source_offset: usize = 0; |
| 542 | const Position = struct { |
| 543 | source: usize, |
| 544 | target: usize, |
| 545 | }; |
| 546 | var var_stack = std.ArrayList(Position).init(allocator); |
| 547 | defer var_stack.deinit(); |
| 548 | loop: while (curr < contents.len) : (curr += 1) { |
| 549 | switch (contents[curr]) { |
| 550 | '@' => blk: { |
| 551 | if (std.mem.indexOfScalarPos(u8, contents, curr + 1, '@')) |close_pos| { |
| 552 | if (close_pos == curr + 1) { |
| 553 | // closed immediately, preserve as a literal |
| 554 | break :blk; |
| 555 | } |
| 556 | const valid_varname_end = std.mem.indexOfNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0; |
| 557 | if (valid_varname_end != close_pos) { |
| 558 | // contains invalid characters, preserve as a literal |
| 559 | break :blk; |
| 560 | } |
| 536 | 561 | |
| 537 | | switch (value) { |
| 538 | | .boolean => |b| { |
| 539 | | const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, @intFromBool(b), endline }); |
| 540 | | last_index = prefix_index + 1; |
| 562 | const key = contents[curr + 1 .. close_pos]; |
| 563 | const value = values.get(key) orelse .undef; |
| 564 | const missing = contents[source_offset..curr]; |
| 565 | try result.appendSlice(missing); |
| 566 | switch (value) { |
| 567 | .undef, .defined => {}, |
| 568 | .boolean => |b| { |
| 569 | try result.append(if (b) '1' else '0'); |
| 570 | }, |
| 571 | .int => |i| { |
| 572 | try result.writer().print("{d}", .{i}); |
| 573 | }, |
| 574 | .ident, .string => |s| { |
| 575 | try result.appendSlice(s); |
| 576 | }, |
| 577 | } |
| 541 | 578 | |
| 542 | | allocator.free(content_buf); |
| 543 | | content_buf = buf; |
| 544 | | }, |
| 545 | | .int => |i| { |
| 546 | | const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, i, endline }); |
| 547 | | const isNegative = i < 0; |
| 548 | | const digits = (if (0 < i) std.math.log10(@abs(i)) else 0) + 1; |
| 549 | | last_index = prefix_index + @intFromBool(isNegative) + digits; |
| 579 | curr = close_pos; |
| 580 | source_offset = close_pos + 1; |
| 550 | 581 | |
| 551 | | allocator.free(content_buf); |
| 552 | | content_buf = buf; |
| 553 | | }, |
| 554 | | .string, .ident => |x| { |
| 555 | | const buf = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ beginline, x, endline }); |
| 556 | | last_index = prefix_index + x.len; |
| 582 | continue :loop; |
| 583 | } |
| 584 | }, |
| 585 | '$' => blk: { |
| 586 | const next = curr + 1; |
| 587 | if (next == contents.len or contents[next] != '{') { |
| 588 | // no open bracket detected, preserve as a literal |
| 589 | break :blk; |
| 590 | } |
| 591 | const missing = contents[source_offset..curr]; |
| 592 | try result.appendSlice(missing); |
| 593 | try result.appendSlice(open_var); |
| 594 | |
| 595 | source_offset = curr + open_var.len; |
| 596 | curr = next; |
| 597 | try var_stack.append(Position{ |
| 598 | .source = curr, |
| 599 | .target = result.items.len - open_var.len, |
| 600 | }); |
| 601 | |
| 602 | continue :loop; |
| 603 | }, |
| 604 | '}' => blk: { |
| 605 | if (var_stack.items.len == 0) { |
| 606 | // no open bracket, preserve as a literal |
| 607 | break :blk; |
| 608 | } |
| 609 | const open_pos = var_stack.pop(); |
| 610 | if (source_offset == open_pos.source) { |
| 611 | source_offset += open_var.len; |
| 612 | } |
| 613 | const missing = contents[source_offset..curr]; |
| 614 | try result.appendSlice(missing); |
| 557 | 615 | |
| 558 | | allocator.free(content_buf); |
| 559 | | content_buf = buf; |
| 560 | | }, |
| 616 | const key_start = open_pos.target + open_var.len; |
| 617 | const key = result.items[key_start..]; |
| 618 | const value = values.get(key) orelse .undef; |
| 619 | result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len); |
| 620 | switch (value) { |
| 621 | .undef, .defined => {}, |
| 622 | .boolean => |b| { |
| 623 | try result.append(if (b) '1' else '0'); |
| 624 | }, |
| 625 | .int => |i| { |
| 626 | try result.writer().print("{d}", .{i}); |
| 627 | }, |
| 628 | .ident, .string => |s| { |
| 629 | try result.appendSlice(s); |
| 630 | }, |
| 631 | } |
| 561 | 632 | |
| 562 | | else => { |
| 563 | | const buf = try std.fmt.allocPrint(allocator, "{s}{s}", .{ beginline, endline }); |
| 564 | | last_index = prefix_index; |
| 633 | source_offset = curr + 1; |
| 565 | 634 | |
| 566 | | allocator.free(content_buf); |
| 567 | | content_buf = buf; |
| 568 | | }, |
| 569 | | } |
| 570 | | continue; |
| 635 | continue :loop; |
| 636 | }, |
| 637 | '\\' => { |
| 638 | // backslash is not considered a special character |
| 639 | continue :loop; |
| 640 | }, |
| 641 | else => {}, |
| 642 | } |
| 643 | |
| 644 | if (var_stack.items.len > 0 and std.mem.indexOfScalar(u8, valid_varname_chars, contents[curr]) == null) { |
| 645 | return error.InvalidCharacter; |
| 571 | 646 | } |
| 647 | } |
| 572 | 648 | |
| 573 | | last_index = start_index + 1; |
| 649 | if (source_offset != contents.len) { |
| 650 | const missing = contents[source_offset..]; |
| 651 | try result.appendSlice(missing); |
| 574 | 652 | } |
| 575 | 653 | |
| 576 | | return content_buf; |
| 654 | return result.toOwnedSlice(); |
| 655 | } |
| 656 | |
| 657 | fn testReplaceVariables( |
| 658 | allocator: Allocator, |
| 659 | contents: []const u8, |
| 660 | expected: []const u8, |
| 661 | values: std.StringArrayHashMap(Value), |
| 662 | ) !void { |
| 663 | const actual = try expand_variables_cmake(allocator, contents, values); |
| 664 | defer allocator.free(actual); |
| 665 | |
| 666 | try std.testing.expectEqualStrings(expected, actual); |
| 667 | } |
| 668 | |
| 669 | test "expand_variables_cmake simple cases" { |
| 670 | const allocator = std.testing.allocator; |
| 671 | var values = std.StringArrayHashMap(Value).init(allocator); |
| 672 | defer values.deinit(); |
| 673 | |
| 674 | try values.putNoClobber("undef", .undef); |
| 675 | try values.putNoClobber("defined", .defined); |
| 676 | try values.putNoClobber("true", Value{ .boolean = true }); |
| 677 | try values.putNoClobber("false", Value{ .boolean = false }); |
| 678 | try values.putNoClobber("int", Value{ .int = 42 }); |
| 679 | try values.putNoClobber("ident", Value{ .string = "value" }); |
| 680 | try values.putNoClobber("string", Value{ .string = "text" }); |
| 681 | |
| 682 | // empty strings are preserved |
| 683 | try testReplaceVariables(allocator, "", "", values); |
| 684 | |
| 685 | // line with misc content is preserved |
| 686 | try testReplaceVariables(allocator, "no substitution", "no substitution", values); |
| 687 | |
| 688 | // empty ${} wrapper is removed |
| 689 | try testReplaceVariables(allocator, "${}", "", values); |
| 690 | |
| 691 | // empty @ sigils are preserved |
| 692 | try testReplaceVariables(allocator, "@", "@", values); |
| 693 | try testReplaceVariables(allocator, "@@", "@@", values); |
| 694 | try testReplaceVariables(allocator, "@@@", "@@@", values); |
| 695 | try testReplaceVariables(allocator, "@@@@", "@@@@", values); |
| 696 | |
| 697 | // simple substitution |
| 698 | try testReplaceVariables(allocator, "@undef@", "", values); |
| 699 | try testReplaceVariables(allocator, "${undef}", "", values); |
| 700 | try testReplaceVariables(allocator, "@defined@", "", values); |
| 701 | try testReplaceVariables(allocator, "${defined}", "", values); |
| 702 | try testReplaceVariables(allocator, "@true@", "1", values); |
| 703 | try testReplaceVariables(allocator, "${true}", "1", values); |
| 704 | try testReplaceVariables(allocator, "@false@", "0", values); |
| 705 | try testReplaceVariables(allocator, "${false}", "0", values); |
| 706 | try testReplaceVariables(allocator, "@int@", "42", values); |
| 707 | try testReplaceVariables(allocator, "${int}", "42", values); |
| 708 | try testReplaceVariables(allocator, "@ident@", "value", values); |
| 709 | try testReplaceVariables(allocator, "${ident}", "value", values); |
| 710 | try testReplaceVariables(allocator, "@string@", "text", values); |
| 711 | try testReplaceVariables(allocator, "${string}", "text", values); |
| 712 | |
| 713 | // double packed substitution |
| 714 | try testReplaceVariables(allocator, "@string@@string@", "texttext", values); |
| 715 | try testReplaceVariables(allocator, "${string}${string}", "texttext", values); |
| 716 | |
| 717 | // triple packed substitution |
| 718 | try testReplaceVariables(allocator, "@string@@int@@string@", "text42text", values); |
| 719 | try testReplaceVariables(allocator, "@string@${int}@string@", "text42text", values); |
| 720 | try testReplaceVariables(allocator, "${string}@int@${string}", "text42text", values); |
| 721 | try testReplaceVariables(allocator, "${string}${int}${string}", "text42text", values); |
| 722 | |
| 723 | // double separated substitution |
| 724 | try testReplaceVariables(allocator, "@int@.@int@", "42.42", values); |
| 725 | try testReplaceVariables(allocator, "${int}.${int}", "42.42", values); |
| 726 | |
| 727 | // triple separated substitution |
| 728 | try testReplaceVariables(allocator, "@int@.@true@.@int@", "42.1.42", values); |
| 729 | try testReplaceVariables(allocator, "@int@.${true}.@int@", "42.1.42", values); |
| 730 | try testReplaceVariables(allocator, "${int}.@true@.${int}", "42.1.42", values); |
| 731 | try testReplaceVariables(allocator, "${int}.${true}.${int}", "42.1.42", values); |
| 732 | |
| 733 | // misc prefix is preserved |
| 734 | try testReplaceVariables(allocator, "false is @false@", "false is 0", values); |
| 735 | try testReplaceVariables(allocator, "false is ${false}", "false is 0", values); |
| 736 | |
| 737 | // misc suffix is preserved |
| 738 | try testReplaceVariables(allocator, "@true@ is true", "1 is true", values); |
| 739 | try testReplaceVariables(allocator, "${true} is true", "1 is true", values); |
| 740 | |
| 741 | // surrounding content is preserved |
| 742 | try testReplaceVariables(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); |
| 743 | try testReplaceVariables(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values); |
| 744 | |
| 745 | // incomplete key is preserved |
| 746 | try testReplaceVariables(allocator, "@undef", "@undef", values); |
| 747 | try testReplaceVariables(allocator, "${undef", "${undef", values); |
| 748 | try testReplaceVariables(allocator, "{undef}", "{undef}", values); |
| 749 | try testReplaceVariables(allocator, "undef@", "undef@", values); |
| 750 | try testReplaceVariables(allocator, "undef}", "undef}", values); |
| 751 | |
| 752 | // unknown key is removed |
| 753 | try testReplaceVariables(allocator, "@bad@", "", values); |
| 754 | try testReplaceVariables(allocator, "${bad}", "", values); |
| 755 | } |
| 756 | |
| 757 | test "expand_variables_cmake edge cases" { |
| 758 | const allocator = std.testing.allocator; |
| 759 | var values = std.StringArrayHashMap(Value).init(allocator); |
| 760 | defer values.deinit(); |
| 761 | |
| 762 | // special symbols |
| 763 | try values.putNoClobber("at", Value{ .string = "@" }); |
| 764 | try values.putNoClobber("dollar", Value{ .string = "$" }); |
| 765 | try values.putNoClobber("underscore", Value{ .string = "_" }); |
| 766 | |
| 767 | // basic value |
| 768 | try values.putNoClobber("string", Value{ .string = "text" }); |
| 769 | |
| 770 | // proxy case values |
| 771 | try values.putNoClobber("string_proxy", Value{ .string = "string" }); |
| 772 | try values.putNoClobber("string_at", Value{ .string = "@string@" }); |
| 773 | try values.putNoClobber("string_curly", Value{ .string = "{string}" }); |
| 774 | try values.putNoClobber("string_var", Value{ .string = "${string}" }); |
| 775 | |
| 776 | // stack case values |
| 777 | try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" }); |
| 778 | try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" }); |
| 779 | |
| 780 | // @-vars resolved only when they wrap valid characters, otherwise considered literals |
| 781 | try testReplaceVariables(allocator, "@@string@@", "@text@", values); |
| 782 | try testReplaceVariables(allocator, "@${string}@", "@text@", values); |
| 783 | |
| 784 | // @-vars are resolved inside ${}-vars |
| 785 | try testReplaceVariables(allocator, "${@string_proxy@}", "text", values); |
| 786 | |
| 787 | // expanded variables are considered strings after expansion |
| 788 | try testReplaceVariables(allocator, "@string_at@", "@string@", values); |
| 789 | try testReplaceVariables(allocator, "${string_at}", "@string@", values); |
| 790 | try testReplaceVariables(allocator, "$@string_curly@", "${string}", values); |
| 791 | try testReplaceVariables(allocator, "$${string_curly}", "${string}", values); |
| 792 | try testReplaceVariables(allocator, "${string_var}", "${string}", values); |
| 793 | try testReplaceVariables(allocator, "@string_var@", "${string}", values); |
| 794 | try testReplaceVariables(allocator, "${dollar}{${string}}", "${text}", values); |
| 795 | try testReplaceVariables(allocator, "@dollar@{${string}}", "${text}", values); |
| 796 | try testReplaceVariables(allocator, "@dollar@{@string@}", "${text}", values); |
| 797 | |
| 798 | // when expanded variables contain invalid characters, they prevent further expansion |
| 799 | try testReplaceVariables(allocator, "${${string_var}}", "", values); |
| 800 | try testReplaceVariables(allocator, "${@string_var@}", "", values); |
| 801 | |
| 802 | // nested expanded variables are expanded from the inside out |
| 803 | try testReplaceVariables(allocator, "${string${underscore}proxy}", "string", values); |
| 804 | try testReplaceVariables(allocator, "${string@underscore@proxy}", "string", values); |
| 805 | |
| 806 | // nested vars are only expanded when ${} is closed |
| 807 | try testReplaceVariables(allocator, "@nest@underscore@proxy@", "underscore", values); |
| 808 | try testReplaceVariables(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values); |
| 809 | try testReplaceVariables(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "underscore", values); |
| 810 | try testReplaceVariables(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values); |
| 811 | |
| 812 | // invalid characters lead to an error |
| 813 | try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str*ing}", "", values)); |
| 814 | try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str$ing}", "", values)); |
| 815 | try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str@ing}", "", values)); |
| 816 | } |
| 817 | |
| 818 | test "expand_variables_cmake escaped characters" { |
| 819 | const allocator = std.testing.allocator; |
| 820 | var values = std.StringArrayHashMap(Value).init(allocator); |
| 821 | defer values.deinit(); |
| 822 | |
| 823 | try values.putNoClobber("string", Value{ .string = "text" }); |
| 824 | |
| 825 | // backslash is an invalid character for @ lookup |
| 826 | try testReplaceVariables(allocator, "\\@string\\@", "\\@string\\@", values); |
| 827 | |
| 828 | // backslash is preserved, but doesn't affect ${} variable expansion |
| 829 | try testReplaceVariables(allocator, "\\${string}", "\\text", values); |
| 830 | |
| 831 | // backslash breaks ${} opening bracket identification |
| 832 | try testReplaceVariables(allocator, "$\\{string}", "$\\{string}", values); |
| 833 | |
| 834 | // backslash is skipped when checking for invalid characters, yet it mangles the key |
| 835 | try testReplaceVariables(allocator, "${string\\}", "", values); |
| 577 | 836 | } |