diff --git a/assets/ObfuscatorTest.pbix b/assets/ObfuscatorTest.pbix deleted file mode 100644 index 35927d4..0000000 Binary files a/assets/ObfuscatorTest.pbix and /dev/null differ diff --git a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs index f597444..48ab9cc 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs @@ -35,12 +35,9 @@ internal string DeobfuscateExpression(string expression) case DaxToken.DELIMITED_COMMENT: tokenText = _dictionary.GetValue(tokenText); break; - case DaxToken.COLUMN_OR_MEASURE when token.IsReservedExtensionColumn(): + case DaxToken.COLUMN_OR_MEASURE when token.IsReservedTokenName(): tokenText = token.Replace(expression, tokenText); break; - case DaxToken.STRING_LITERAL when token.IsExtensionColumnName(): - tokenText = ReplaceExtensionColumnName(token); - break; case DaxToken.TABLE_OR_VARIABLE when token.IsVariable(): case DaxToken.TABLE: case DaxToken.COLUMN_OR_MEASURE: @@ -48,7 +45,18 @@ internal string DeobfuscateExpression(string expression) case DaxToken.UNTERMINATED_COLREF: case DaxToken.UNTERMINATED_TABLEREF: case DaxToken.UNTERMINATED_STRING: - tokenText = token.Replace(expression, _dictionary.GetValue(tokenText)); + { + if (token.Text.IsFullyQualifiedColumnName()) + { + var value = DeobfuscateFullyQualifiedColumnName(tokenText).EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + else + { + var value = _dictionary.GetValue(tokenText).EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + } break; } @@ -56,15 +64,14 @@ internal string DeobfuscateExpression(string expression) } return builder.ToString(); + } - string ReplaceExtensionColumnName(DaxToken token) - { - var (tableName, columnName) = token.GetExtensionColumnNameParts(); - tableName = _dictionary.GetValue(tableName); - columnName = _dictionary.GetValue(columnName); + internal string DeobfuscateFullyQualifiedColumnName(string value) + { + var (table, column) = value.GetFullyQualifiedColumnNameParts(); + var tableName = _dictionary.GetValue(table); + var columnName = _dictionary.GetValue(column); - var value = $"{tableName.DaxEscape()}[{columnName.DaxEscape()}]"; - return token.Replace(expression, value, escape: true); - } + return $"{tableName}[{columnName}]"; } } diff --git a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs index 531001e..eae2562 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs @@ -113,8 +113,16 @@ private void Deobfuscate(DaxName name) { if (string.IsNullOrWhiteSpace(name?.Name)) return; - var value = _dictionary.GetValue(name!.Name); - name.Name = value; + if (name!.Name.IsFullyQualifiedColumnName()) + { + var value = DeobfuscateFullyQualifiedColumnName(name!.Name); + name.Name = value; + } + else + { + var value = _dictionary.GetValue(name!.Name); + name.Name = value; + } } private void Deobfuscate(DaxNote note) diff --git a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs index 3fd3fa3..0aa7414 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs @@ -1,6 +1,6 @@ -using Dax.Vpax.Obfuscator.Extensions; -using System.Text; +using System.Text; using Dax.Tokenizer; +using Dax.Vpax.Obfuscator.Extensions; namespace Dax.Vpax.Obfuscator; @@ -35,12 +35,9 @@ internal string ObfuscateExpression(string expression) case DaxToken.DELIMITED_COMMENT: tokenText = ObfuscateText(new DaxText(tokenText)).ObfuscatedValue; break; - case DaxToken.COLUMN_OR_MEASURE when token.IsReservedExtensionColumn(): + case DaxToken.COLUMN_OR_MEASURE when token.IsReservedTokenName(): tokenText = token.Replace(expression, tokenText); break; - case DaxToken.STRING_LITERAL when token.IsExtensionColumnName(): - tokenText = ReplaceExtensionColumnName(token); - break; case DaxToken.TABLE_OR_VARIABLE when token.IsVariable(): case DaxToken.TABLE: case DaxToken.COLUMN_OR_MEASURE: @@ -48,7 +45,18 @@ internal string ObfuscateExpression(string expression) case DaxToken.UNTERMINATED_COLREF: case DaxToken.UNTERMINATED_TABLEREF: case DaxToken.UNTERMINATED_STRING: - tokenText = token.Replace(expression, ObfuscateText(new DaxText(tokenText))); + { + if (token.Text.IsFullyQualifiedColumnName()) + { + var value = ObfuscateFullyQualifiedColumnName(tokenText).EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + else + { + var value = ObfuscateText(new DaxText(tokenText)).ObfuscatedValue.EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + } break; } @@ -56,15 +64,14 @@ internal string ObfuscateExpression(string expression) } return builder.ToString(); + } - string ReplaceExtensionColumnName(DaxToken token) - { - var (tableName, columnName) = token.GetExtensionColumnNameParts(); - var tableText = ObfuscateText(new DaxText(tableName)); - var columnText = ObfuscateText(new DaxText(columnName)); + internal string ObfuscateFullyQualifiedColumnName(string value) + { + var (table, column) = value.GetFullyQualifiedColumnNameParts(obfuscating: true); + var tableName = ObfuscateText(new DaxText(table)).ObfuscatedValue; + var columnName = ObfuscateText(new DaxText(column)).ObfuscatedValue; - var value = $"{tableText.ObfuscatedValue.DaxEscape()}[{columnText.ObfuscatedValue.DaxEscape()}]"; - return token.Replace(expression, value, escape: true); - } + return $"{tableName}[{columnName}]"; } } diff --git a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs index dd548a7..4291b53 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs @@ -55,7 +55,8 @@ private void ObfuscateIdentifiers(Column column) private void ObfuscateIdentifiers(Measure measure) { - var measureText = Obfuscate(measure.MeasureName) ?? throw new InvalidOperationException($"The measure name is not valid [{measure.MeasureName}]."); + var name = measure.MeasureName.Name; + var obfuscatedName = Obfuscate(measure.MeasureName) ?? throw new InvalidOperationException($"The measure name is not valid [{name}]."); CreateKpiMeasure(measure.KpiTargetExpression, "Goal"); CreateKpiMeasure(measure.KpiStatusExpression, "Status"); CreateKpiMeasure(measure.KpiTrendExpression, "Trend"); @@ -64,8 +65,8 @@ void CreateKpiMeasure(DaxExpression kpi, string type) { if (string.IsNullOrWhiteSpace(kpi?.Expression)) return; - var text = new DaxText($"_{measureText.Value} {type}"); - text.ObfuscatedValue = $"_{measureText.ObfuscatedValue} {type}"; + var text = new DaxText($"_{name} {type}"); + text.ObfuscatedValue = $"_{obfuscatedName} {type}"; // It may already exist in case of incremental obfuscation if (Texts.IsIncrementalObfuscation && Texts.Contains(text)) @@ -151,13 +152,22 @@ private void Obfuscate(TablePermission tablePermission) Obfuscate(tablePermission.FilterExpression); } - private DaxText? Obfuscate(DaxName name) + private string? Obfuscate(DaxName name) { if (string.IsNullOrWhiteSpace(name?.Name)) return null; - var text = ObfuscateText(new DaxText(name!.Name)); - name.Name = text.ObfuscatedValue; - return text; + if (name!.Name.IsFullyQualifiedColumnName()) + { + var value = ObfuscateFullyQualifiedColumnName(name!.Name); + name.Name = value; + } + else + { + var text = ObfuscateText(new DaxText(name!.Name)); + name.Name = text.ObfuscatedValue; + } + + return name.Name; } private void Obfuscate(DaxNote note) diff --git a/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs b/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs index 5d81823..857d245 100644 --- a/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs +++ b/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs @@ -65,12 +65,11 @@ private static bool IsReservedChar(char @char) { // Reserved characters are preserved during obfuscation - switch (@char) { - case '-': // single-line comment char + switch (@char) + { + case ReservedChar_Minus: // single-line comment char case '/': // multi-line comment char case '*': // multi-line comment char - case ']': // square bracket escape char e.g. Sales[Rate[%]]] - case '"': // quotation mark escape char e.g. VAR __quotationMarkChar = """" case '\n': // line feed char e.g. in multi-line comments case '\r': // carriage return char e.g. in multi-line comments return true; @@ -79,6 +78,7 @@ private static bool IsReservedChar(char @char) return false; } + internal const char ReservedChar_Minus = '-'; /// /// CALENDAR() [Date] extension column. /// diff --git a/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs b/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs index 0377fe2..ebe2573 100644 --- a/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs +++ b/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs @@ -5,15 +5,15 @@ namespace Dax.Vpax.Obfuscator.Extensions; internal static class DaxTokenExtensions { - public static bool IsExtensionColumnName(this DaxToken token) - => token.Type == DaxToken.STRING_LITERAL && token.Text.EndsWith("]") && token.Text.IndexOf('[') > 0; - public static bool IsVariable(this DaxToken token) - => token.Type == DaxToken.TABLE_OR_VARIABLE && !IsFunction(token); + { + Debug.Assert(token.Type == DaxToken.TABLE_OR_VARIABLE); + return token.Type == DaxToken.TABLE_OR_VARIABLE && !IsFunction(token); + } public static bool IsFunction(this DaxToken token) { - if (token.Type != DaxToken.TABLE_OR_VARIABLE) return false; + Debug.Assert(token.Type == DaxToken.TABLE_OR_VARIABLE); var current = token.Next; while (current != null && current.CommentOrWhitespace) @@ -22,9 +22,9 @@ public static bool IsFunction(this DaxToken token) return current != null && current.Type == DaxToken.OPEN_PARENS; } - public static bool IsReservedExtensionColumn(this DaxToken token) + public static bool IsReservedTokenName(this DaxToken token) { - if (token.Type != DaxToken.COLUMN_OR_MEASURE) return false; + Debug.Assert(token.Type == DaxToken.COLUMN_OR_MEASURE); if (token.Text.StartsWith(DaxTextObfuscator.ReservedToken_Value, StringComparison.OrdinalIgnoreCase)) { @@ -44,28 +44,21 @@ public static bool IsReservedExtensionColumn(this DaxToken token) return false; } - public static (string tableName, string columnName) GetExtensionColumnNameParts(this DaxToken token) - { - Debug.Assert(token.IsExtensionColumnName()); - - var openIndex = token.Text.IndexOf('['); - var closeIndex = token.Text.LastIndexOf(']'); - var tableName = token.Text.Substring(0, openIndex); - var columnName = token.Text.Substring(openIndex + 1, closeIndex - openIndex - 1); - return (tableName, columnName); - } - - public static string Replace(this DaxToken token, string expression, DaxText text) - => Replace(token, expression, text.ObfuscatedValue); - - public static string Replace(this DaxToken token, string expression, string value, bool escape = false) + public static string Replace(this DaxToken token, string expression, string value) { var substring = expression.Substring(token.StartIndex, token.StopIndex - token.StartIndex + 1); - var tokenText = escape ? token.Text.DaxEscape() : token.Text; - if (substring.IndexOf(tokenText, StringComparison.Ordinal) == -1) - throw new InvalidOperationException($"Failed to replace token >> {token.Type} | {substring} | {tokenText} | {value}"); + switch (token.Type) + { + case DaxToken.TABLE: + case DaxToken.STRING_LITERAL: + case DaxToken.COLUMN_OR_MEASURE: + return string.Concat(substring[0], value, substring[substring.Length - 1]); + case DaxToken.UNTERMINATED_TABLEREF: + case DaxToken.UNTERMINATED_COLREF: + return string.Concat(substring[0], value); + } - return substring.Replace(tokenText, value); + return substring.Replace(token.Text, value); } } diff --git a/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs b/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs index 10a8ccc..f8323bb 100644 --- a/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs +++ b/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs @@ -1,7 +1,70 @@ -namespace Dax.Vpax.Obfuscator.Extensions; +using System.Diagnostics; +using Dax.Tokenizer; + +namespace Dax.Vpax.Obfuscator.Extensions; internal static class StringExtensions { - public static string DaxEscape(this string value) - => value.Replace("\"", "\"\"").Replace("'", "''"); + public static bool IsFullyQualifiedColumnName(this string value) + => value.TrimEnd().EndsWith("]") && value.IndexOf('[') > 0; + + public static (string table, string column) GetFullyQualifiedColumnNameParts(this string value, bool obfuscating = false) + { + Debug.Assert(IsFullyQualifiedColumnName(value)); + + var openIndex = value.IndexOf('['); + var closeIndex = value.LastIndexOf(']'); + var table = value.Substring(0, openIndex); + var column = value.Substring(openIndex + 1, closeIndex - openIndex - 1); + + if (obfuscating) + { + table = table.Trim(); // remove any leading or trailing whitespace first + + if (IsSquareBraketsRequired(table, column)) + { + // Since the plaintext value contains at least one character that results in a fully qualified + // column name that requires square brackets, then, in order to preserve the same semantics + // we must add at least a single char of the same type to the obfuscated value as well. + table = $"{DaxTextObfuscator.ReservedChar_Minus}{table}"; + } + } + + return (table, column); + + static bool IsSquareBraketsRequired(string table, string column) + { + if (table.Length > 0) + { + if ("012345679".Contains(table[0])) + return true; // Table name start with a digit + + if (table.Any((c) => c != '_' && !DaxTextObfuscator.CharSet.Contains(c))) + return true; // Table name contains any non-alphabetic characters except for the underscore + } + + return column.Contains(']'); + } + } + + public static string EscapeDax(this string value, int tokenType) + { + // See _action() methods in Dax.Tokenizer.DaxLexer class + + switch (tokenType) + { + case DaxToken.TABLE: + case DaxToken.UNTERMINATED_TABLEREF: + return value.Replace("'", "''"); + + case DaxToken.STRING_LITERAL: + return value.Replace("\"", "\"\""); + + case DaxToken.COLUMN_OR_MEASURE: + case DaxToken.UNTERMINATED_COLREF: + return value.Replace("]", "]]"); + } + + return value; + } } diff --git a/src/Dax.Vpax.Obfuscator/version.json b/src/Dax.Vpax.Obfuscator/version.json index 1d8af53..77031d7 100644 --- a/src/Dax.Vpax.Obfuscator/version.json +++ b/src/Dax.Vpax.Obfuscator/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.3-beta", + "version": "0.4-beta", "nugetPackageVersion": { "semVer": 2.0 }, diff --git a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs index 035ad1b..2116aaa 100644 --- a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs +++ b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs @@ -108,15 +108,15 @@ public void DeobfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsDeobf } [Fact] - public void DeobfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsDeobfuscatedColumnNamePartsPreservingQuotationMarkEscapeChar() + public void DeobfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsDeobfuscatedColumnNamePartsWithoutPreservingQuotationMarkEscapeChar() { - var expression = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[Y""Y]", 1), XXX[Y"Y]) """; + var expression = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[YYY]", 1), XXX[YYY]) """; var expected = """ SELECTCOLUMNS(ADDCOLUMNS({}, "aaa[b""c]", 1), aaa[b"c]) """; var (_, _, deobfuscator) = CreateTest( [ new ObfuscationText("aaa", "XXX"), - new ObfuscationText("b\"c", "Y\"Y"), + new ObfuscationText("b\"c", "YYY"), ]); var actual = deobfuscator.DeobfuscateExpression(expression); @@ -141,13 +141,14 @@ public void DeobfuscateExpression_TableNameMultipleReferencesWithDifferentCasing [Fact] public void DeobfuscateExpression_ColumnName_ReturnsDeobfuscatedValuePreservingSquareBracketEscapeChar() { - var expression = "RELATED( XXXXX[YYYYYY]]] )"; + var expression = "RELATED( XXXXX[YYYY[Z]]] )"; var expected = "RELATED( Sales[Rate[%]]] )"; var (_, _, deobfuscator) = CreateTest( [ new ObfuscationText("Sales", "XXXXX"), - new ObfuscationText("Rate[%]", "YYYYYY]") + new ObfuscationText("Rate", "YYYY"), + new ObfuscationText("%", "Z") ]); var actual = deobfuscator.DeobfuscateExpression(expression); @@ -170,7 +171,7 @@ public void DeobfuscateExpression_VariableNameMultipleReferencesWithDifferentCas } [Fact] - public void ObfuscateExpression_ValueExtensionColumnName_IsNotObfuscated() + public void DebfuscateExpression_ValueExtensionColumnName_IsNotDeobfuscatedBecauseItIsNotObfuscated() { var expression = """ SELECTCOLUMNS({0}, "XXXXXXXXXX", ''[Value]) """; var expected = """ SELECTCOLUMNS({0}, "__Measures", ''[Value]) """; @@ -185,7 +186,7 @@ public void ObfuscateExpression_ValueExtensionColumnName_IsNotObfuscated() } [Fact] - public void DeobfuscateExpression_EmptyStringLiteral_IsNotDeobfuscatedBecauseItIsNotObfuscated() + public void DeobfuscateExpression_StringLiteralEmpty_IsNotDeobfuscatedBecauseItIsNotObfuscated() { var expected = """ IF("" = "", "", "") """; @@ -195,6 +196,23 @@ public void DeobfuscateExpression_EmptyStringLiteral_IsNotDeobfuscatedBecauseItI Assert.Equal(expected, actual); } + [Fact] + public void ObfuscateExpression_StringLiteralWithEscapedQuotationMark_IsObfuscated() + { + var expression = """"" "X" & "Y" & "Z" """""; + var expected = """"" "A" & """" & "B" """""; + + var (_, _, deobfuscator) = CreateTest( + [ + new ObfuscationText("A", "X"), + new ObfuscationText("\"", "Y"), + new ObfuscationText("B", "Z"), + ]); + var actual = deobfuscator.DeobfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + private (Model model, ObfuscationDictionary dictionary, DaxModelDeobfuscator deobfuscator) CreateTest(ObfuscationText[] texts) { var dictionary = new ObfuscationDictionary(id: Guid.NewGuid().ToString("D"), texts); diff --git a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs index 4977c40..ee61bf2 100644 --- a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs +++ b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs @@ -95,14 +95,56 @@ public void ObfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsObfusca } [Fact] - public void ObfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsObfuscatedColumnNamePartsPreservingQuotationMarkEscapeChar() + public void ObfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsObfuscatedColumnNamePartsWithoutPreservingQuotationMarkEscapeChar() { var expression = """ SELECTCOLUMNS(ADDCOLUMNS({}, "aaa[b""c]", 1), aaa[b"c]) """; - var expected = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[Y""Y]", 1), XXX[Y"Y]) """; + var expected = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[YYY]", 1), XXX[YYY]) """; var obfuscator = new DaxModelObfuscator(new Model()); obfuscator.Texts.Add(new DaxText("aaa", "XXX")); - obfuscator.Texts.Add(new DaxText("b\"c", "Y\"Y")); + obfuscator.Texts.Add(new DaxText("b\"c", "YYY")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ObfuscateExpression_ExtensionColumnNameFullyQualifiedWithSquareBracket_Test1() + { + var expression = """ SUMX(ADDCOLUMNS({}, "@rate[%]", 1), [@rate[%]]]) """; + var expected = """ SUMX(ADDCOLUMNS({}, "-XXXXX[Y]", 1), [-XXXXX[Y]]]) """; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("-@rate", "-XXXXX")); + obfuscator.Texts.Add(new DaxText("%", "Y")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ObfuscateExpression_ExtensionColumnNameFullyQualifiedWithSquareBracket_Test2() + { + var expression = """ SUMX(ADDCOLUMNS({}, " col11 [ a] b ] ", 1), [ col11 [ a]] b ]] ]) """; + var expected = """ SUMX(ADDCOLUMNS({}, "-XXXXX[YYYYYY]", 1), [-XXXXX[YYYYYY]]]) """; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("-col11", "-XXXXX")); + obfuscator.Texts.Add(new DaxText(" a] b ", "YYYYYY")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ObfuscateExpression_ExtensionColumnNameFullyQualifiedWithSquareBracket_Test3() + { + var expression = """ SUMX(ADDCOLUMNS({}, " col15 [ a ""' b ] ", 1), col15[ a "' b ]) """; + var expected = """ SUMX(ADDCOLUMNS({}, "XXXXX[YYYYYYYY]", 1), XXXXX[YYYYYYYY]) """; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("col15", "XXXXX")); + obfuscator.Texts.Add(new DaxText(" a \"' b ", "YYYYYYYY")); var actual = obfuscator.ObfuscateExpression(expression); Assert.Equal(expected, actual); @@ -125,11 +167,12 @@ public void ObfuscateExpression_TableNameWithDifferentCasings_ReturnsSameObfusca public void ObfuscateExpression_ColumnName_ReturnsObfuscatedValuePreservingSquareBracketEscapeChar() { var expression = "RELATED( Sales[Rate[%]]] )"; - var expected = "RELATED( XXXXX[YYYYYY]]] )"; + var expected = "RELATED( XXXXX[YYYY[Z]]] )"; var obfuscator = new DaxModelObfuscator(new Model()); obfuscator.Texts.Add(new DaxText("Sales", "XXXXX")); - obfuscator.Texts.Add(new DaxText("Rate[%]", "YYYYYY]")); + obfuscator.Texts.Add(new DaxText("Rate", "YYYY")); + obfuscator.Texts.Add(new DaxText("%", "Z")); var actual = obfuscator.ObfuscateExpression(expression); Assert.Equal(expected, actual); @@ -162,7 +205,7 @@ public void ObfuscateExpression_ValueExtensionColumnName_IsNotObfuscated() } [Fact] - public void ObfuscateExpression_EmptyStringLiteral_IsNotObfuscated() + public void ObfuscateExpression_StringLiteralEmpty_IsNotObfuscated() { var expected = """ IF("" = "", "", "") """; @@ -172,6 +215,21 @@ public void ObfuscateExpression_EmptyStringLiteral_IsNotObfuscated() Assert.Equal(expected, actual); } + [Fact] + public void ObfuscateExpression_StringLiteralWithEscapedQuotationMark_IsObfuscated() + { + var expression = """"" "A" & """" & "B" """""; + var expected = """"" "X" & "Y" & "Z" """""; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("A", "X")); + obfuscator.Texts.Add(new DaxText("\"", "Y")); + obfuscator.Texts.Add(new DaxText("B", "Z")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + [Theory] [InlineData(nameof(DaxToken.DISPLAYFOLDER))] [InlineData(nameof(DaxToken.FORMATSTRING))] diff --git a/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs b/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs index 5d0e85a..b017b52 100644 --- a/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs +++ b/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs @@ -28,14 +28,6 @@ public void Obfuscate_SameValueUsingDifferentDaxTextObfuscatorInstances_ReturnsD Assert.NotEqual(text1.ObfuscatedValue, text2.ObfuscatedValue); } - [Fact] - public void Obfuscate_EscapedQuotationMarkInStringLiteral_ReturnsUnobfuscatedQuotationMark() - { - var value = "\"\"\"\""; // e.g. VAR __quotationMarkChar = """" - var text = _obfuscator.Obfuscate(new DaxText(value)); - Assert.Equal(value, text.ObfuscatedValue); - } - [Theory] [InlineData(DaxTextObfuscator.CharSet)] [InlineData("Sales Amount")] diff --git a/tests/pbix/ObfuscatorTest.pbix b/tests/pbix/ObfuscatorTest.pbix new file mode 100644 index 0000000..795682f Binary files /dev/null and b/tests/pbix/ObfuscatorTest.pbix differ