Skip to content

Commit 4533f73

Browse files
authored
Fixes quotes get incorrectly duplicated Azure#2593 (Azure#2612)
1 parent da9632b commit 4533f73

9 files changed

+187
-7
lines changed

docs/CHANGELOG-v1.md

+6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
3232

3333
## Unreleased
3434

35+
What's changed since v1.32.0:
36+
37+
- Bug fixes:
38+
- Fixed quotes get incorrectly duplicated by @BernieWhite.
39+
[#2593](https://github.com/Azure/PSRule.Rules.Azure/issues/2593)
40+
3541
## v1.32.0
3642

3743
What's changed since v1.31.3:

src/PSRule.Rules.Azure/Data/Template/ExpressionParser.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

44
using System.Threading;

src/PSRule.Rules.Azure/Data/Template/ExpressionStream.cs

+29-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.ComponentModel.Design;
56
using System.Diagnostics;
67
using System.Linq;
78
using System.Threading;
@@ -140,7 +141,7 @@ public bool CaptureString(out string s)
140141
Next();
141142
SkipQuotePairs(ref length);
142143
}
143-
s = Substring(start, length);
144+
s = Substring(start, length, ignoreDoubleQuotes: false);
144145
IsString();
145146
return true;
146147
}
@@ -215,7 +216,7 @@ private void SkipQuotePairs(ref int length)
215216
{
216217
while (!IsEscaped && Apostrophe == Current && Offset(1, out var offset) && offset == Apostrophe)
217218
{
218-
length += 2;
219+
length += 1;
219220
Next();
220221
Next();
221222
}
@@ -240,20 +241,40 @@ private void UpdateCurrent(bool ignoreEscaping = false)
240241
_Current = _Source[_Position + _EscapeLength];
241242
}
242243

244+
/// <summary>
245+
/// Count excape is used to offset a position if an escape sequence exists.
246+
/// </summary>
243247
private int GetEscapeCount(int position)
244248
{
245-
// Check for escape sequences
249+
// Check for escape sequences.
246250
if (position < _Length && _Source[position] == Backslash)
247251
{
248252
var next = _Source[position + 1];
249253

250-
// Check against list of escapable characters
254+
// Check against list of escapable characters.
251255
if (next is Backslash or BracketOpen or ParenthesesOpen or BracketClose or ParenthesesClose)
252256
return 1;
253257
}
254258
return 0;
255259
}
256260

261+
/// <summary>
262+
/// Count dobule quote is used to offset a postion if double quoting exists with an apostrophe.
263+
/// </summary>
264+
private int GetDoubleQuoteCount(int position)
265+
{
266+
// Check for apostrophe quote character.
267+
if (position < _Length && _Source[position] == Apostrophe)
268+
{
269+
var next = _Source[position + 1];
270+
271+
// Check for secondary apostrophe quote character.
272+
if (next is Apostrophe)
273+
return 1;
274+
}
275+
return 0;
276+
}
277+
257278
public string CaptureUntil(char[] c, bool ignoreEscaping = false)
258279
{
259280
var start = Position;
@@ -269,7 +290,7 @@ public string CaptureUntil(char[] c, bool ignoreEscaping = false)
269290
return Substring(start, length, ignoreEscaping);
270291
}
271292

272-
private string Substring(int start, int length, bool ignoreEscaping = false)
293+
private string Substring(int start, int length, bool ignoreEscaping = false, bool ignoreDoubleQuotes = true)
273294
{
274295
if (ignoreEscaping)
275296
return _Source.Substring(start, length);
@@ -280,6 +301,9 @@ private string Substring(int start, int length, bool ignoreEscaping = false)
280301
while (i < length)
281302
{
282303
var offset = GetEscapeCount(position);
304+
if (!ignoreDoubleQuotes && offset == 0)
305+
offset = GetDoubleQuoteCount(position);
306+
283307
buffer[i] = _Source[position + offset];
284308
position += offset + 1;
285309
i++;

tests/PSRule.Rules.Azure.Tests/ExpressionBuilderTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public void JsonQuoteNesting()
143143
var context = GetContext();
144144
var actual = Build(context, expression) as JObject;
145145
Assert.NotNull(actual);
146-
Assert.Equal("[parameters(''effect'')]", actual["effect"].Value<string>());
146+
Assert.Equal("[parameters('effect')]", actual["effect"].Value<string>());
147147

148148
expression = "[json('{ \"value\": \"[int(last(split(replace(field(''test''), ''t'', ''''), ''/'')))]\" }')]";
149149
actual = Build(context, expression) as JObject;

tests/PSRule.Rules.Azure.Tests/ExpressionParserTests.cs

+26
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,31 @@ public void ParseExpressionWithStringIndexProperty()
193193
Assert.Equal("item1", actual[14].Content);
194194
Assert.Equal(ExpressionTokenType.IndexEnd, actual[15].Type);
195195
}
196+
197+
[Fact]
198+
public void ParseQuoting()
199+
{
200+
var expression = "[format('A''{0}''', string(variables('task').parameters))]";
201+
var actual = ExpressionParser.Parse(expression).ToArray();
202+
203+
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // format
204+
Assert.Equal("format", actual[0].Content);
205+
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
206+
Assert.Equal(ExpressionTokenType.String, actual[2].Type); // 'A'{0}''
207+
Assert.Equal("A'{0}'", actual[2].Content);
208+
Assert.Equal(ExpressionTokenType.Element, actual[3].Type); // string
209+
Assert.Equal("string", actual[3].Content);
210+
Assert.Equal(ExpressionTokenType.GroupStart, actual[4].Type);
211+
Assert.Equal(ExpressionTokenType.Element, actual[5].Type); // variables
212+
Assert.Equal("variables", actual[5].Content);
213+
Assert.Equal(ExpressionTokenType.GroupStart, actual[6].Type);
214+
Assert.Equal(ExpressionTokenType.String, actual[7].Type); // 'task'
215+
Assert.Equal("task", actual[7].Content);
216+
Assert.Equal(ExpressionTokenType.GroupEnd, actual[8].Type);
217+
Assert.Equal(ExpressionTokenType.Property, actual[9].Type); // parameters
218+
Assert.Equal("parameters", actual[9].Content);
219+
Assert.Equal(ExpressionTokenType.GroupEnd, actual[10].Type);
220+
Assert.Equal(ExpressionTokenType.GroupEnd, actual[11].Type);
221+
}
196222
}
197223
}

tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@
215215
<None Update="Tests.Bicep.32.json">
216216
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
217217
</None>
218+
<None Update="Tests.Bicep.33.json">
219+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
220+
</None>
218221
<None Update="Tests.Bicep.4.json">
219222
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
220223
</None>

tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs

+13
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,19 @@ public void DependencyOrdering()
10441044
Assert.Equal("dep_subnet4_2", actual["properties"]["tags"]["deployment"].Value<string>());
10451045
}
10461046

1047+
[Fact]
1048+
public void Quoting()
1049+
{
1050+
var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.33.json"), null, out var templateContext);
1051+
Assert.Equal(3, resources.Length);
1052+
1053+
Assert.True(templateContext.RootDeployment.TryOutput("outTask", out JObject result));
1054+
Assert.Equal(5, result["value"]["parameters"]["B"].Value<int>());
1055+
Assert.True(templateContext.RootDeployment.TryOutput("outTasks", out result));
1056+
Assert.Equal("A'{\"B\":5}'", result["value"][0]["parameters"]["debug"].Value<string>());
1057+
Assert.Equal(10, result["value"][0]["parameters"]["debugLength"].Value<int>());
1058+
}
1059+
10471060
#region Helper methods
10481061

10491062
private static string GetSourcePath(string fileName)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Community provided sample from: https://github.com/Azure/PSRule.Rules.Azure/issues/2593
5+
6+
var task = {
7+
name: 'task1'
8+
parameters: {
9+
B: 5
10+
}
11+
}
12+
13+
var tasks = [
14+
{
15+
name: task.name
16+
parameters: {
17+
debug: 'A\'${string(task.parameters)}\''
18+
debugLength: length('A\'${string(task.parameters)}\'')
19+
}
20+
}
21+
]
22+
23+
#disable-next-line BCP081 no-deployments-resources
24+
resource taskDeployment 'Microsoft.Resources/deployments@2020-10-01' = {
25+
name: 'name'
26+
properties: {
27+
mode: 'Incremental'
28+
template: {
29+
'$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
30+
contentVersion: '1.0.0.0'
31+
resources: [
32+
{
33+
apiVersion: '2019-12-01'
34+
type: 'Microsoft.ManagedIdentity/userAssignedIdentities'
35+
name: 'test'
36+
properties: {
37+
tasks: tasks
38+
}
39+
}
40+
]
41+
}
42+
}
43+
}
44+
45+
output outTask object = task
46+
output outTasks array = tasks
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
3+
"contentVersion": "1.0.0.0",
4+
"metadata": {
5+
"_generator": {
6+
"name": "bicep",
7+
"version": "0.24.24.22086",
8+
"templateHash": "9157129407835921175"
9+
}
10+
},
11+
"variables": {
12+
"task": {
13+
"name": "task1",
14+
"parameters": {
15+
"B": 5
16+
}
17+
},
18+
"tasks": [
19+
{
20+
"name": "[variables('task').name]",
21+
"parameters": {
22+
"debug": "[format('A''{0}''', string(variables('task').parameters))]",
23+
"debugLength": "[length(format('A''{0}''', string(variables('task').parameters)))]"
24+
}
25+
}
26+
]
27+
},
28+
"resources": [
29+
{
30+
"type": "Microsoft.Resources/deployments",
31+
"apiVersion": "2020-10-01",
32+
"name": "name",
33+
"properties": {
34+
"mode": "Incremental",
35+
"template": {
36+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
37+
"contentVersion": "1.0.0.0",
38+
"resources": [
39+
{
40+
"apiVersion": "2019-12-01",
41+
"type": "Microsoft.ManagedIdentity/userAssignedIdentities",
42+
"name": "test",
43+
"properties": {
44+
"tasks": "[variables('tasks')]"
45+
}
46+
}
47+
]
48+
}
49+
}
50+
}
51+
],
52+
"outputs": {
53+
"outTask": {
54+
"type": "object",
55+
"value": "[variables('task')]"
56+
},
57+
"outTasks": {
58+
"type": "array",
59+
"value": "[variables('tasks')]"
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)