Skip to content

Commit 907f518

Browse files
tigcursoragent
andauthored
Fixes NativeAOT IL2026 in RuneJsonConverter. (#5659)
Parse glyph/U+hex/\u config strings directly instead of SerializeToUtf8Bytes + JsonSerializerOptions.Default, which ILC flags as IL2026 for AOT consumers (tig/winprint#272). Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 2e5c2dd commit 907f518

2 files changed

Lines changed: 146 additions & 103 deletions

File tree

Terminal.Gui/Configuration/RuneJsonConverter.cs

Lines changed: 101 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ namespace Terminal.Gui.Configuration;
2121
internal class RuneJsonConverter : JsonConverter<Rune>
2222
{
2323
/// <summary>
24-
/// Parses a configuration string using the same rules as <see cref="Read"/>.
24+
/// Parses a configuration string using the same rules as <see cref="Read"/> for a JSON string token.
25+
/// Does not round-trip through <see cref="JsonSerializer"/> (that path is trim-unsafe on NativeAOT).
2526
/// </summary>
2627
internal static bool TryParse (string? value, out Rune result)
2728
{
@@ -34,15 +35,7 @@ internal static bool TryParse (string? value, out Rune result)
3435

3536
try
3637
{
37-
byte [] utf8 = JsonSerializer.SerializeToUtf8Bytes (value);
38-
Utf8JsonReader reader = new (utf8);
39-
40-
if (!reader.Read ())
41-
{
42-
return false;
43-
}
44-
45-
result = new RuneJsonConverter ().Read (ref reader, typeof (Rune), JsonSerializerOptions.Default);
38+
result = ParseString (value);
4639

4740
return true;
4841
}
@@ -57,99 +50,7 @@ public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSe
5750
switch (reader.TokenType)
5851
{
5952
case JsonTokenType.String:
60-
{
61-
string? value = reader.GetString ();
62-
int first = RuneExtensions.MaxUnicodeCodePoint + 1;
63-
int second = RuneExtensions.MaxUnicodeCodePoint + 1;
64-
65-
if (value is { } && (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase)
66-
|| value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase)))
67-
{
68-
// Handle encoded single char, surrogate pair, or combining mark + char
69-
uint [] codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})")
70-
.Select (
71-
match => uint.Parse (
72-
match.Groups [1].Value,
73-
NumberStyles.HexNumber
74-
)
75-
)
76-
.ToArray ();
77-
78-
if (codePoints.Length is 0 or > 2)
79-
{
80-
throw new JsonException ($"{value}: Invalid Rune.");
81-
}
82-
83-
if (codePoints.Length > 0)
84-
{
85-
first = (int)codePoints [0];
86-
}
87-
88-
if (codePoints.Length == 2)
89-
{
90-
second = (int)codePoints [1];
91-
}
92-
}
93-
else
94-
{
95-
// Handle single character, surrogate pair, or combining mark + char
96-
if (value is { Length: 0 or > 2 })
97-
{
98-
throw new JsonException ($"{value}: Invalid Rune");
99-
}
100-
101-
if (value is { Length: > 0 })
102-
{
103-
first = value [0];
104-
}
105-
106-
if (value is { Length: 2 })
107-
{
108-
second = value [1];
109-
}
110-
}
111-
112-
Rune result;
113-
114-
if (second == RuneExtensions.MaxUnicodeCodePoint + 1)
115-
{
116-
// Single codepoint
117-
if (!Rune.TryCreate (first, out result))
118-
{
119-
throw new JsonException ($"{value}: Invalid Rune");
120-
}
121-
122-
return result;
123-
}
124-
125-
// Surrogate pair?
126-
if (Rune.TryCreate ((char)first, (char)second, out result))
127-
{
128-
return result;
129-
}
130-
131-
if (!Rune.IsValid (second))
132-
{
133-
throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not valid: {second}.");
134-
}
135-
136-
var cm = new Rune (second);
137-
138-
if (!cm.IsCombiningMark ())
139-
{
140-
throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not a combining mark: {cm}.");
141-
}
142-
143-
// not a surrogate pair, so a combining mark + char?
144-
string combined = string.Concat ((char)first, (char)second).Normalize ();
145-
146-
if (!Rune.IsValid (combined [0]))
147-
{
148-
throw new JsonException ($"{value}: Invalid combined Rune.");
149-
}
150-
151-
return new (combined [0]);
152-
}
53+
return ParseString (reader.GetString ());
15354
case JsonTokenType.Number:
15455
{
15556
uint num = reader.GetUInt32 ();
@@ -182,5 +83,102 @@ public override void Write (Utf8JsonWriter writer, Rune value, JsonSerializerOpt
18283
writer.WriteStringValue (value.ToString ());
18384
}
18485
}
86+
87+
/// <summary>
88+
/// Parses a glyph, U+hex, or \u string using the same rules as a JSON string token in <see cref="Read"/>.
89+
/// </summary>
90+
private static Rune ParseString (string? value)
91+
{
92+
int first = RuneExtensions.MaxUnicodeCodePoint + 1;
93+
int second = RuneExtensions.MaxUnicodeCodePoint + 1;
94+
95+
if (value is { } && (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase)
96+
|| value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase)))
97+
{
98+
// Handle encoded single char, surrogate pair, or combining mark + char
99+
uint [] codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})")
100+
.Select (
101+
match => uint.Parse (
102+
match.Groups [1].Value,
103+
NumberStyles.HexNumber
104+
)
105+
)
106+
.ToArray ();
107+
108+
if (codePoints.Length is 0 or > 2)
109+
{
110+
throw new JsonException ($"{value}: Invalid Rune.");
111+
}
112+
113+
if (codePoints.Length > 0)
114+
{
115+
first = (int)codePoints [0];
116+
}
117+
118+
if (codePoints.Length == 2)
119+
{
120+
second = (int)codePoints [1];
121+
}
122+
}
123+
else
124+
{
125+
// Handle single character, surrogate pair, or combining mark + char
126+
if (value is { Length: 0 or > 2 })
127+
{
128+
throw new JsonException ($"{value}: Invalid Rune");
129+
}
130+
131+
if (value is { Length: > 0 })
132+
{
133+
first = value [0];
134+
}
135+
136+
if (value is { Length: 2 })
137+
{
138+
second = value [1];
139+
}
140+
}
141+
142+
Rune result;
143+
144+
if (second == RuneExtensions.MaxUnicodeCodePoint + 1)
145+
{
146+
// Single codepoint
147+
if (!Rune.TryCreate (first, out result))
148+
{
149+
throw new JsonException ($"{value}: Invalid Rune");
150+
}
151+
152+
return result;
153+
}
154+
155+
// Surrogate pair?
156+
if (Rune.TryCreate ((char)first, (char)second, out result))
157+
{
158+
return result;
159+
}
160+
161+
if (!Rune.IsValid (second))
162+
{
163+
throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not valid: {second}.");
164+
}
165+
166+
var cm = new Rune (second);
167+
168+
if (!cm.IsCombiningMark ())
169+
{
170+
throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not a combining mark: {cm}.");
171+
}
172+
173+
// not a surrogate pair, so a combining mark + char?
174+
string combined = string.Concat ((char)first, (char)second).Normalize ();
175+
176+
if (!Rune.IsValid (combined [0]))
177+
{
178+
throw new JsonException ($"{value}: Invalid combined Rune.");
179+
}
180+
181+
return new (combined [0]);
182+
}
185183
}
186184
#pragma warning restore 1591

Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,49 @@ public void Json_With_Decimal_Works ()
143143
// Assert
144144
Assert.Equal ("a", deserialized.ToString ());
145145
}
146+
147+
// CoPilot - Grok 4.6
148+
[Theory]
149+
[InlineData ("a", "a")]
150+
[InlineData ("☑", "☑")]
151+
[InlineData ("\\u2611", "☑")]
152+
[InlineData ("U+2611", "☑")]
153+
[InlineData ("🍎", "🍎")]
154+
[InlineData ("U+1F34E", "🍎")]
155+
[InlineData ("\\U0001F34E", "🍎")]
156+
[InlineData ("\\ud83d \\udc69", "👩")]
157+
[InlineData ("\\ud83d\\udc69", "👩")]
158+
[InlineData ("U+d83d U+dc69", "👩")]
159+
[InlineData ("U+1F469", "👩")]
160+
[InlineData ("\\U0001F469", "👩")]
161+
[InlineData ("\\u0065\\u0301", "é")]
162+
[InlineData ("6", "6")]
163+
public void TryParse_DoesNotNeedJsonSerializer_Positive (string rune, string expected)
164+
{
165+
Assert.True (RuneJsonConverter.TryParse (rune, out Rune result));
166+
Assert.Equal (expected, result.ToString ());
167+
}
168+
169+
// CoPilot - Grok 4.6
170+
[Theory]
171+
[InlineData (null)]
172+
[InlineData ("")]
173+
[InlineData ("aa")]
174+
[InlineData ("☑☑")]
175+
[InlineData ("\\x2611")]
176+
[InlineData ("Z+2611")]
177+
[InlineData ("🍎🍎")]
178+
[InlineData ("U+FFF1F34E")]
179+
[InlineData ("\\UFFF1F34E")]
180+
[InlineData ("\\ud83d")]
181+
[InlineData ("\\udc3d")]
182+
[InlineData ("\\ud83d \\u1c69")]
183+
[InlineData ("\\ud83ddc69")]
184+
[InlineData ("U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467")]
185+
[InlineData ("\\U0001F469\\u200D\\U0001F469\\u200D\\U0001F467\\u200D\\U0001F467")]
186+
[InlineData ("9733")]
187+
public void TryParse_DoesNotNeedJsonSerializer_Negative (string? rune)
188+
{
189+
Assert.False (RuneJsonConverter.TryParse (rune, out _));
190+
}
146191
}

0 commit comments

Comments
 (0)