Skip to content

Commit

Permalink
Add validation for null props inside objects inside arrays. (Azure#4896)
Browse files Browse the repository at this point in the history
Add validation for null props inside objects inside arrays.
  • Loading branch information
vadim-kovalyov authored May 4, 2021
1 parent 00e39b9 commit f96961f
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,40 @@ public class ReportedPropertiesValidator : IValidator<TwinCollection>
public void Validate(TwinCollection reportedProperties)
{
Preconditions.CheckNotNull(reportedProperties, nameof(reportedProperties));

JToken reportedPropertiesJToken = JToken.Parse(reportedProperties.ToJson());
ValidateTwinProperties(reportedPropertiesJToken, 1);
ValidateTwinCollectionSize(reportedProperties);
ValidateArrayContent(reportedPropertiesJToken);
// root level has no property name.
ValidateToken(string.Empty, reportedPropertiesJToken, 0, false);
}

static void ValidateTwinProperties(JToken properties, int currentDepth)
static void ValidateToken(string name, JToken item, int currentDepth, bool inArray)
{
foreach (JProperty kvp in ((JObject)properties).Properties())
{
ValidatePropertyNameAndLength(kvp.Name);
ValidatePropertyNameAndLength(name);

ValidateValueType(kvp.Name, kvp.Value);
if (item is JObject @object)
{
ValidateTwinDepth(currentDepth);

if (kvp.Value is JValue)
// do validation recursively
foreach (JProperty kvp in @object.Properties())
{
if (kvp.Value.Type is JTokenType.Integer)
{
ValidateIntegerValue(kvp.Name, (long)kvp.Value);
}
else
{
string s = kvp.Value.ToString();
ValidatePropertyValueLength(kvp.Name, s);
}
ValidateToken(kvp.Name, kvp.Value, currentDepth + 1, inArray);
}
}

if (kvp.Value != null && kvp.Value is JObject)
{
if (currentDepth > TwinPropertyMaxDepth)
{
throw new InvalidOperationException($"Nested depth of twin property exceeds {TwinPropertyMaxDepth}");
}
if (item is JValue value)
{
ValidateValueType(name, value);
ValidateValue(name, value, inArray);
}

// do validation recursively
ValidateTwinProperties(kvp.Value, currentDepth + 1);
}
if (item is JArray array)
{
ValidateTwinDepth(currentDepth);

// do array validation
ValidateArrayContent(array, currentDepth + 1);
}
}

Expand Down Expand Up @@ -95,30 +92,48 @@ static void ValidatePropertyValueLength(string name, string value)
}
}

static void ValidateArrayContent(JToken token)
static void ValidateArrayContent(JArray array, int currentDepth)
{
switch (token)
foreach (var item in array)
{
case JArray array:
foreach (var item in array)
{
if (item.Type == JTokenType.Null)
{
throw new InvalidOperationException("Arrays cannot contain 'null' as value");
}
if (item.Type is JTokenType.Null)
{
throw new InvalidOperationException("Arrays cannot contain 'null' as value");
}

ValidateArrayContent(item);
if (item is JArray inner)
{
if (currentDepth > TwinPropertyMaxDepth)
{
throw new InvalidOperationException($"Nested depth of twin property exceeds {TwinPropertyMaxDepth}");
}

break;
// do array validation
ValidateArrayContent(inner, currentDepth + 1);
}
else
{
// items in the array don't have property name.
ValidateToken(string.Empty, item, currentDepth, true);
}
}
}

case JObject @object:
foreach (var item in @object)
{
ValidateArrayContent(item.Value);
}
static void ValidateValue(string name, JValue value, bool inArray)
{
if (inArray && value.Type is JTokenType.Null)
{
throw new InvalidOperationException($"Property {name} of an object in an array cannot be 'null'");
}

break;
if (value.Type is JTokenType.Integer)
{
ValidateIntegerValue(name, (long)value);
}
else
{
string s = value.ToString();
ValidatePropertyValueLength(name, s);
}
}

Expand Down Expand Up @@ -147,5 +162,13 @@ static void ValidateTwinCollectionSize(TwinCollection collection)
throw new InvalidOperationException($"Twin properties size {size} exceeds maximum {TwinPropertyDocMaxLength}");
}
}

static void ValidateTwinDepth(int currentDepth)
{
if (currentDepth > TwinPropertyMaxDepth)
{
throw new InvalidOperationException($"Nested depth of twin property exceeds {TwinPropertyMaxDepth}");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,57 @@ public static IEnumerable<object[]> GetTwinCollections()
"Nested depth of twin property exceeds 10"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
level1 = new
{
// level 2
array1 = new[]
{
// level 3
new[]
{
// level 4
new[]
{
// level 5
new[]
{
// level 6
new[]
{
// level 7
new[]
{
// level 8
new[]
{
// level 9
new[]
{
// level 10
new[]
{
// level 11
new[] { "one", "two", "three" },
}
}
}
}
}
}
}
},
}
}
})),
typeof(InvalidOperationException),
"Nested depth of twin property exceeds 10"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
Expand Down Expand Up @@ -197,8 +248,33 @@ public static IEnumerable<object[]> GetTwinCollections()
yield return new object[]
{
new TwinCollection("{ \"ok\": [\"good\"], \"ok2\": [], \"level1\": [{ \"field1\": null }] }"),
null,
string.Empty
typeof(InvalidOperationException),
"Property field1 of an object in an array cannot be 'null'"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
complex = new
{
array1 = new object[]
{
"one",
"two",
new
{
array2 = new[]
{
new { hello = (string)null }
}
},
}
}
})),
typeof(InvalidOperationException),
"Property hello of an object in an array cannot be 'null'"
};

yield return new object[]
Expand Down Expand Up @@ -266,6 +342,7 @@ public static IEnumerable<object[]> GetTwinCollections()
{
new[] { "one", "two", "three" },
new[] { "four", "five", "six" },
new object[] { "seven", new { ok = "ok" } },
},
pi = 3.14,
sometime = new DateTime(2021, 1, 20),
Expand Down

0 comments on commit f96961f

Please sign in to comment.