Skip to content

Commit 06645ea

Browse files
committed
Simplify SanitizeBytes and use stack-based brace fixing
- Replace manual byte-by-byte loop with Array.IndexOf + Array.FindAll - Refactor TryFixBraces to use a Stack<char> for correct LIFO ordering of mixed {} and [] bracket pairs - Add test for mixed bracket truncation recovery https://claude.ai/code/session_01Gej4ey7eUsbPRDqFWwxKyZ
1 parent f6b138c commit 06645ea

2 files changed

Lines changed: 67 additions & 74 deletions

File tree

StabilityMatrix.Core/Services/SettingsJsonSanitizer.cs

Lines changed: 54 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,20 @@ public static class SettingsJsonSanitizer
1616
/// </summary>
1717
public static byte[] SanitizeBytes(byte[] rawBytes)
1818
{
19-
// Check if any null bytes exist first (fast path for clean files)
20-
var hasNullBytes = false;
21-
foreach (var b in rawBytes)
22-
{
23-
if (b == 0x00)
24-
{
25-
hasNullBytes = true;
26-
break;
27-
}
28-
}
29-
30-
if (!hasNullBytes)
19+
// Fast path for clean files
20+
if (Array.IndexOf(rawBytes, (byte)0x00) < 0)
3121
return rawBytes;
3222

33-
// Filter out null bytes
34-
var result = new byte[rawBytes.Length];
35-
var writeIndex = 0;
36-
foreach (var b in rawBytes)
37-
{
38-
if (b != 0x00)
39-
{
40-
result[writeIndex++] = b;
41-
}
42-
}
43-
44-
return result.AsSpan(0, writeIndex).ToArray();
23+
return Array.FindAll(rawBytes, b => b != 0x00);
4524
}
4625

4726
/// <summary>
48-
/// Ensures the JSON text has matching curly braces by appending missing closing braces.
27+
/// Ensures the JSON text has matching brackets by appending missing closing braces/brackets.
28+
/// Uses a stack to correctly handle nested and mixed <c>{}</c> / <c>[]</c> pairs.
4929
/// </summary>
5030
public static string TryFixBraces(string jsonText)
5131
{
52-
var openCount = 0;
32+
var stack = new Stack<char>();
5333
var inString = false;
5434
var escaped = false;
5535

@@ -79,73 +59,73 @@ public static string TryFixBraces(string jsonText)
7959
switch (c)
8060
{
8161
case '{':
82-
openCount++;
62+
stack.Push('}');
8363
break;
84-
case '}':
85-
openCount--;
64+
case '[':
65+
stack.Push(']');
66+
break;
67+
case '}' or ']' when stack.Count > 0:
68+
stack.Pop();
8669
break;
8770
}
8871
}
8972

90-
if (openCount > 0)
91-
{
92-
// Trim trailing garbage after the last valid content
93-
var trimmed = jsonText.TrimEnd();
73+
if (stack.Count == 0)
74+
return jsonText;
9475

95-
// If we end in the middle of a value (e.g. truncated number or string),
96-
// try to find the last complete property by removing the trailing partial content
97-
if (trimmed.Length > 0)
98-
{
99-
var lastChar = trimmed[^1];
100-
// If the last char isn't a valid JSON value terminator, trim back to the last comma or brace
101-
if (lastChar != '"' && lastChar != '}' && lastChar != ']'
102-
&& !char.IsDigit(lastChar) && lastChar != 'e' && lastChar != 'l'
103-
&& lastChar != 's' && lastChar != ',')
104-
{
105-
// Find the last comma, closing bracket, or opening brace
106-
var lastSafe = trimmed.LastIndexOfAny([',', '}', ']', '{']);
107-
if (lastSafe > 0)
108-
{
109-
trimmed = trimmed[..(lastSafe + 1)];
110-
}
111-
}
76+
// Trim trailing garbage after the last valid content
77+
var trimmed = jsonText.TrimEnd();
11278

113-
// Remove trailing comma before we add closing braces
114-
trimmed = trimmed.TrimEnd();
115-
if (trimmed.EndsWith(','))
79+
// If we end in the middle of a value (e.g. truncated number or string),
80+
// trim back to the last structural character
81+
if (trimmed.Length > 0)
82+
{
83+
var lastChar = trimmed[^1];
84+
if (lastChar != '"' && lastChar != '}' && lastChar != ']'
85+
&& !char.IsDigit(lastChar) && lastChar != 'e' && lastChar != 'l'
86+
&& lastChar != 's' && lastChar != ',')
87+
{
88+
var lastSafe = trimmed.LastIndexOfAny([',', '}', ']', '{', '[']);
89+
if (lastSafe > 0)
11690
{
117-
trimmed = trimmed[..^1];
91+
trimmed = trimmed[..(lastSafe + 1)];
11892
}
11993
}
12094

121-
// Re-count braces after trimming
122-
openCount = 0;
123-
inString = false;
124-
escaped = false;
125-
foreach (var c in trimmed)
95+
// Remove trailing comma before we add closing brackets
96+
trimmed = trimmed.TrimEnd();
97+
if (trimmed.EndsWith(','))
12698
{
127-
if (escaped) { escaped = false; continue; }
128-
if (c == '\\' && inString) { escaped = true; continue; }
129-
if (c == '"') { inString = !inString; continue; }
130-
if (inString) continue;
131-
switch (c)
132-
{
133-
case '{': openCount++; break;
134-
case '}': openCount--; break;
135-
}
99+
trimmed = trimmed[..^1];
136100
}
101+
}
137102

138-
// Append missing closing braces
139-
var sb = new StringBuilder(trimmed);
140-
for (var i = 0; i < openCount; i++)
103+
// Re-scan trimmed text to rebuild the stack
104+
stack.Clear();
105+
inString = false;
106+
escaped = false;
107+
foreach (var c in trimmed)
108+
{
109+
if (escaped) { escaped = false; continue; }
110+
if (c == '\\' && inString) { escaped = true; continue; }
111+
if (c == '"') { inString = !inString; continue; }
112+
if (inString) continue;
113+
switch (c)
141114
{
142-
sb.Append('\n').Append('}');
115+
case '{': stack.Push('}'); break;
116+
case '[': stack.Push(']'); break;
117+
case '}' or ']' when stack.Count > 0: stack.Pop(); break;
143118
}
119+
}
144120

145-
return sb.ToString();
121+
// Append missing closing brackets in correct LIFO order
122+
var sb = new StringBuilder(trimmed);
123+
while (stack.Count > 0)
124+
{
125+
sb.Append('\n').Append(stack.Pop());
146126
}
147127

148-
return jsonText;
128+
return sb.ToString();
149129
}
150130

151131
/// <summary>

StabilityMatrix.Tests/Core/SettingsJsonSanitizerTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ public void TryFixBraces_NestedMissingBraces_AppendsMultiple()
6868
Assert.IsNotNull(JsonDocument.Parse(result));
6969
}
7070

71+
[TestMethod]
72+
public void TryFixBraces_MixedBrackets_ClosesInCorrectOrder()
73+
{
74+
// Truncated JSON with an open array inside an object
75+
var input = """{"items": [1, 2, 3""";
76+
77+
var result = SettingsJsonSanitizer.TryFixBraces(input);
78+
79+
// Should close ] then } in correct LIFO order
80+
Assert.IsNotNull(JsonDocument.Parse(result));
81+
Assert.IsTrue(result.TrimEnd().EndsWith('}'));
82+
}
83+
7184
[TestMethod]
7285
public void TryDeserializeWithRecovery_ValidJson_ReturnsSettings()
7386
{

0 commit comments

Comments
 (0)