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