diff --git a/packages/cdktf/lib/terraform-functions.ts b/packages/cdktf/lib/terraform-functions.ts index 2390f0e795..56c876792e 100644 --- a/packages/cdktf/lib/terraform-functions.ts +++ b/packages/cdktf/lib/terraform-functions.ts @@ -3,6 +3,7 @@ import { call } from "./tfExpression"; import { IResolvable } from "./tokens/resolvable"; import { TokenMap } from "./tokens/private/token-map"; import { TokenString } from "./tokens/private/encoding"; +import { rawString } from "."; // We use branding here to ensure we internally only handle validated values // this allows us to catch usage errors before terraform does in some cases @@ -11,6 +12,12 @@ type TFValueValidator = (value: any) => TFValue; type ExecutableTfFunction = (...args: any[]) => IResolvable; +function hasUnescapedDoubleQuotes(str: string) { + const ret = /\\([\s\S])|(")/.test(str); + console.log("hasUnescapedDoubleQuotes", str, ret); + return ret; +} + // Validators function anyValue(value: any): any { return value; @@ -22,8 +29,15 @@ function mapValue(value: any): any { function stringValue(value: any): any { if (typeof value !== "string" && !Tokenization.isResolvable(value)) { - throw new Error(`${value} is not a valid number nor a token`); + throw new Error(`'${value}' is not a valid string nor a token`); + } + + if (typeof value === "string" && hasUnescapedDoubleQuotes(value)) { + throw new Error( + `'${value}' can not be used as value directly since it has unescaped double quotes in it. To safely use the value please use Fn.rawString on your string.` + ); } + return value; } @@ -1249,4 +1263,13 @@ export class Fn { public static try(expression: any[]) { return asAny(terraformFunction("try", listOf(anyValue))(...expression)); } + + /** + * Use this function to wrap a string and escape it properly for the use in Terraform + * This is only needed in certain scenarios (e.g., if you have unescaped double quotes in the string) + * @param {String} str + */ + public static rawString(str: string): string { + return asString(rawString(str)); + } } diff --git a/packages/cdktf/lib/tfExpression.ts b/packages/cdktf/lib/tfExpression.ts index 4d1d206593..d3cbbedef6 100644 --- a/packages/cdktf/lib/tfExpression.ts +++ b/packages/cdktf/lib/tfExpression.ts @@ -33,8 +33,8 @@ class TFExpression extends Intrinsic implements IResolvable { * Escape string removes characters from the string that are not allowed in Terraform or JSON * It must only be used on non-token values */ - private escapeString(str: string) { - return str + protected escapeString(str: string) { + return str // Escape double quotes .replace(/\n/g, "\\n") // escape newlines .replace(/\${/g, "$$${"); // escape ${ to $${ } @@ -45,7 +45,9 @@ class TFExpression extends Intrinsic implements IResolvable { // String literal if (numberOfTokens === 0) { - return resolvedArg.startsWith('"') && resolvedArg.endsWith('"') + return resolvedArg !== `"` && + resolvedArg.startsWith('"') && + resolvedArg.endsWith('"') ? this.escapeString(resolvedArg) : `"${this.escapeString(resolvedArg)}"`; } @@ -77,6 +79,25 @@ class TFExpression extends Intrinsic implements IResolvable { } } +// A string that represents an input value to be escaped +class RawString extends TFExpression { + constructor(private readonly str: string) { + super(str); + } + + public resolve() { + return `"${this.escapeString(this.str).replace(/\"/g, '\\"')}"`; // eslint-disable-line no-useless-escape + } + + public toString() { + return this.str; + } +} + +export function rawString(str: string): IResolvable { + return new RawString(str); +} + class Reference extends TFExpression { constructor(private identifier: string) { super(identifier); diff --git a/packages/cdktf/test/functions.test.ts b/packages/cdktf/test/functions.test.ts index 05ed4ec7b3..f0bcca41a7 100644 --- a/packages/cdktf/test/functions.test.ts +++ b/packages/cdktf/test/functions.test.ts @@ -407,3 +407,34 @@ test("undefined and null", () => { }" `); }); + +test("throws error on unescaped double quote string inputs", () => { + expect(() => { + const app = Testing.app(); + const stack = new TerraformStack(app, "test"); + new TerraformOutput(stack, "test-output", { + value: Fn.md5(`"`), + }); + Testing.synth(stack); + }).toThrowErrorMatchingInlineSnapshot( + `"'\\"' can not be used as value directly since it has unescaped double quotes in it. To safely use the value please use Fn.rawString on your string."` + ); +}); + +test("throws no error when wrapping unescaped double quotes in Fn.rawString", () => { + const app = Testing.app(); + const stack = new TerraformStack(app, "test"); + new TerraformOutput(stack, "test-output", { + value: Fn.md5(Fn.rawString(`"`)), + }); + + expect(Testing.synth(stack)).toMatchInlineSnapshot(` + "{ + \\"output\\": { + \\"test-output\\": { + \\"value\\": \\"\${md5(\\\\\\"\\\\\\\\\\\\\\"\\\\\\")}\\" + } + } + }" + `); +}); diff --git a/packages/cdktf/test/tfExpression.test.ts b/packages/cdktf/test/tfExpression.test.ts index a62b4b19a7..161a725c26 100644 --- a/packages/cdktf/test/tfExpression.test.ts +++ b/packages/cdktf/test/tfExpression.test.ts @@ -20,6 +20,7 @@ import { call, subOperation, Expression, + rawString, } from "../lib/tfExpression"; import { resolve } from "../lib/_tokens"; const resolveExpression = (expr: Expression) => resolve(null as any, expr); @@ -163,3 +164,9 @@ test("functions don't escape terraform references that have been tokenized", () ) ).toMatchInlineSnapshot(`"\${length(docker_container.foo.bar)}"`); }); + +test("functions escape string markers", () => { + expect( + resolveExpression(call("length", [rawString(`"`)])) + ).toMatchInlineSnapshot(`"\${length(\\"\\\\\\"\\")}"`); +});