Skip to content

Commit ff503ae

Browse files
authored
Merge pull request #1973 from riganti/fix-STJ-error-path
Fix SystemTextJsonUtils.GetFailurePath
2 parents c2f2b5c + ae3926f commit ff503ae

File tree

2 files changed

+155
-46
lines changed

2 files changed

+155
-46
lines changed

src/Framework/Framework/Utils/SystemTextJsonUtils.cs

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,56 +14,55 @@ static class SystemTextJsonUtils
1414
/// <summary> Returns the property path to an unfinished JSON value </summary>
1515
public static string[] GetFailurePath(ReadOnlySpan<byte> data)
1616
{
17-
// TODO: tests
18-
var reader = new Utf8JsonReader(data, false, default);
19-
var path = new Stack<(string? name, int index)>();
20-
var isArray = false;
21-
int arrayIndex = 0;
22-
while (reader.Read())
17+
// configure higher max depth than default (64), to correctly display path of
18+
// the "max depth exceeded" error
19+
var options = new JsonReaderOptions { MaxDepth = 196 };
20+
var reader = new Utf8JsonReader(data, false, new JsonReaderState(options));
21+
if (!reader.Read())
22+
return [];
23+
return GetFailurePathInternal(ref reader) ?? throw new Exception("No error in specified JSON");
24+
}
25+
26+
private static string[]? GetFailurePathInternal(ref Utf8JsonReader reader)
27+
{
28+
if (reader.TokenType == JsonTokenType.None)
29+
return [];
30+
else if (reader.TokenType == JsonTokenType.StartObject)
31+
{
32+
if (!reader.Read()) return [];
33+
string? lastProperty = null;
34+
while (reader.TokenType == JsonTokenType.PropertyName)
35+
{
36+
lastProperty = reader.GetString().NotNull();
37+
if (!reader.Read())
38+
return [lastProperty];
39+
if (GetFailurePathInternal(ref reader) is {} nestedError)
40+
return [lastProperty, ..nestedError];
41+
if (!reader.Read())
42+
return [];
43+
}
44+
if (reader.TokenType != JsonTokenType.EndObject)
45+
return lastProperty is null ? [] : [lastProperty];
46+
return null;
47+
}
48+
else if (reader.TokenType == JsonTokenType.StartArray)
2349
{
24-
switch (reader.TokenType)
50+
if (!reader.Read()) return ["0"];
51+
int index = 0;
52+
while (reader.TokenType != JsonTokenType.EndArray)
2553
{
26-
case JsonTokenType.StartObject:
27-
if (isArray) {
28-
isArray = false;
29-
path.Push((null, arrayIndex));
30-
}
31-
break;
32-
case JsonTokenType.Comment:
33-
break;
34-
case JsonTokenType.StartArray:
35-
isArray = true;
36-
arrayIndex = 0;
37-
break;
38-
case JsonTokenType.EndArray:
39-
isArray = false;
40-
break;
41-
case JsonTokenType.True:
42-
case JsonTokenType.False:
43-
case JsonTokenType.Number:
44-
case JsonTokenType.String:
45-
case JsonTokenType.Null:
46-
case JsonTokenType.EndObject:
47-
if (!isArray) {
48-
var old = path.Pop();
49-
if (old.name is null) {
50-
isArray = true;
51-
arrayIndex = old.index + 1;
52-
}
53-
}
54-
else {
55-
arrayIndex++;
56-
}
57-
break;
58-
case JsonTokenType.PropertyName:
59-
path.Push((reader.GetString()!, -1));
60-
break;
61-
case JsonTokenType.None:
62-
goto Done;
54+
if (GetFailurePathInternal(ref reader) is {} nestedError)
55+
return [$"{index}", ..nestedError];
56+
index++;
57+
if (!reader.Read() || reader.TokenType == JsonTokenType.None)
58+
return [$"{index}"];
6359
}
60+
return null;
61+
}
62+
else
63+
{
64+
return null;
6465
}
65-
Done:
66-
return path.Reverse().Select(n => n.name ?? n.index.ToString()).ToArray();
6766
}
6867

6968
public static JsonElement? GetPropertyOrNull(this in JsonElement jsonObj, ReadOnlySpan<byte> name) =>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
using DotVVM.Framework.ViewModel.Serialization;
3+
using DotVVM.Framework.Utils;
4+
5+
namespace DotVVM.Framework.Tests.ViewModel
6+
{
7+
[TestClass]
8+
public class JsonUtilsTests
9+
{
10+
const string IrrelevantObjectStart = """
11+
"notthis": { "alsonotthis": "lala", "another": 1 },
12+
"alsonot": [ 1, 2, 3, { "irrelevant": 1, "nope": null, "neither_this": true, "alsonot": false }, [], [[[[], []]]] ],
13+
""";
14+
15+
[DataTestMethod]
16+
[DataRow("{\"viewModel\":", "viewModel")]
17+
[DataRow(
18+
$$"""
19+
{
20+
"test": [
21+
{ "notthis": 1, "myprop": 5},
22+
{
23+
"notthis": 1,
24+
"myprop":
25+
""",
26+
"test/1/myprop"
27+
)]
28+
[DataRow(
29+
$$"""
30+
{ {{IrrelevantObjectStart}}
31+
"test": [
32+
{ "notthis": 1, "myprop": 5},
33+
{
34+
"notthis": 1,
35+
"myprop":
36+
""",
37+
"test/1/myprop"
38+
)]
39+
[DataRow(
40+
$$"""
41+
{ {{IrrelevantObjectStart}}
42+
"test": [ { {{IrrelevantObjectStart}} "myprop":{
43+
""",
44+
"test/0/myprop"
45+
)]
46+
[DataRow(
47+
$$"""
48+
{ {{IrrelevantObjectStart}}
49+
"test2":
50+
""",
51+
"test2"
52+
)]
53+
[DataRow(
54+
$$"""
55+
{ {{IrrelevantObjectStart}}
56+
"test3": { "ok": [1, 2, 3], "alsofine": {}
57+
""",
58+
"test3"
59+
)]
60+
[DataRow(
61+
$$"""
62+
{
63+
"items": [
64+
{ "id": 1 },
65+
""",
66+
"items/1"
67+
)]
68+
[DataRow(
69+
$$"""
70+
{
71+
"items": [
72+
{
73+
"nested": [
74+
1,
75+
2,
76+
""",
77+
"items/0/nested/2"
78+
)]
79+
[DataRow(
80+
$$"""
81+
{
82+
"items": [
83+
""",
84+
"items/0"
85+
)]
86+
[DataRow(
87+
"""
88+
{ "arr": [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1, 2], []]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]],
89+
"prop": 1
90+
""",
91+
"prop"
92+
)]
93+
[DataRow(
94+
"""
95+
[
96+
1, 2,
97+
{ "ok": true },
98+
{ "not_ok":
99+
""",
100+
"3/not_ok"
101+
)]
102+
[DataRow(" ", "")]
103+
public void GetInvalidJsonErrorPath(string json, string expectedPath)
104+
{
105+
var utf8 = StringUtils.Utf8.GetBytes(json);
106+
var path = SystemTextJsonUtils.GetFailurePath(utf8);
107+
Assert.AreEqual(expectedPath, string.Join("/", path));
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)