Skip to content

Commit c31a471

Browse files
committed
fix: guard UnicodeText draw indexing
1 parent e8b6982 commit c31a471

File tree

3 files changed

+222
-15
lines changed

3 files changed

+222
-15
lines changed

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,66 @@ public async Task When_FontFamily_Changed(string font)
739739
Assert.AreNotEqual(originalSize, SUT.DesiredSize);
740740
}
741741

742+
[TestMethod]
743+
[PlatformCondition(ConditionMode.Include, RuntimeTestPlatforms.Skia)]
744+
public async Task When_TextBlock_With_Unpaired_Surrogate_Renders()
745+
{
746+
if (!ApiInformation.IsTypePresent("Microsoft.UI.Xaml.Media.Imaging.RenderTargetBitmap, Uno.UI"))
747+
{
748+
Assert.Inconclusive();
749+
}
750+
751+
var text = "LTR \uD800 RTL שלום";
752+
var sut = new TextBlock
753+
{
754+
Text = text,
755+
TextWrapping = TextWrapping.Wrap
756+
};
757+
sut.TextHighlighters.Add(new TextHighlighter
758+
{
759+
Ranges =
760+
{
761+
new TextRange { StartIndex = 0, Length = text.Length }
762+
}
763+
});
764+
765+
var root = new Border
766+
{
767+
Width = 240,
768+
Height = 120,
769+
Child = sut
770+
};
771+
772+
await UITestHelper.Load(root);
773+
await UITestHelper.ScreenShot(root);
774+
}
775+
776+
[TestMethod]
777+
[PlatformCondition(ConditionMode.Include, RuntimeTestPlatforms.Skia)]
778+
public async Task When_TextBlock_With_Unpaired_Surrogate_And_No_Highlighter_Renders()
779+
{
780+
if (!ApiInformation.IsTypePresent("Microsoft.UI.Xaml.Media.Imaging.RenderTargetBitmap, Uno.UI"))
781+
{
782+
Assert.Inconclusive();
783+
}
784+
785+
var sut = new TextBlock
786+
{
787+
Text = "LTR \uD800 RTL שלום",
788+
TextWrapping = TextWrapping.Wrap
789+
};
790+
791+
var root = new Border
792+
{
793+
Width = 240,
794+
Height = 120,
795+
Child = sut
796+
};
797+
798+
await UITestHelper.Load(root);
799+
await UITestHelper.ScreenShot(root);
800+
}
801+
742802
[TestMethod]
743803
#if !__ANDROID__
744804
[Ignore("Android-only test for AndroidAssets backward compatibility")]

src/Uno.UI.Tests/Windows_UI_XAML_Controls/TextBlockTests/Given_TextBlock.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.UI.Xaml.Controls;
1010
using System.Drawing;
1111
using Microsoft.UI.Xaml.Media;
12+
using Windows.Foundation;
1213

1314
namespace Uno.UI.Tests.Windows_UI_XAML_Controls.TextBlockTests
1415
{
@@ -51,6 +52,55 @@ public void When_Changing_Foreground_Property()
5152
}
5253
#endif
5354

55+
[TestMethod]
56+
public void When_Text_Has_Unpaired_Surrogate_Does_Not_Throw()
57+
{
58+
var tb = new TextBlock
59+
{
60+
Text = "A\uD800B\uDC00"
61+
};
62+
63+
tb.Measure(new Size(300, 80));
64+
}
65+
66+
[TestMethod]
67+
public void When_Text_Has_Unpaired_Surrogate_With_Highlighter_Does_Not_Throw()
68+
{
69+
var tb = new TextBlock
70+
{
71+
Text = "A\uD800B\uDC00"
72+
};
73+
74+
tb.TextHighlighters.Add(new TextHighlighter
75+
{
76+
Ranges =
77+
{
78+
new TextRange { StartIndex = 0, Length = tb.Text.Length }
79+
}
80+
});
81+
82+
tb.Measure(new Size(300, 80));
83+
}
84+
85+
[TestMethod]
86+
public void When_Highlighter_Range_Exceeds_Text_Does_Not_Throw()
87+
{
88+
var tb = new TextBlock
89+
{
90+
Text = "abc"
91+
};
92+
93+
tb.TextHighlighters.Add(new TextHighlighter
94+
{
95+
Ranges =
96+
{
97+
new TextRange { StartIndex = 0, Length = 999 }
98+
}
99+
});
100+
101+
tb.Measure(new Size(300, 80));
102+
}
103+
54104
[TestMethod]
55105
public void When_LineBreak_SurroundingWhiteSpace()
56106
{

src/Uno.UI/UI/Xaml/Documents/UnicodeText.skia.cs

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ internal unsafe UnicodeText(
197197
}
198198
}
199199

200-
_text = stringBuilder.ToString();
200+
_text = SanitizeText(stringBuilder.ToString());
201201
if (_text.Length == 0)
202202
{
203203
_lines = [];
@@ -230,7 +230,7 @@ internal unsafe UnicodeText(
230230
Array.Fill(embeddingLevels, (byte)level, start, count);
231231
}
232232

233-
using var _ = ICU.CreateBiDiAndSetPara(_text, 0, _text.Length, flowDirection is FlowDirection.RightToLeft ? UBIDI_DEFAULT_RTL : UBIDI_DEFAULT_LTR, out var bidi, embeddingLevels);
233+
using var _ = CreateBiDiOrFallback(_text, flowDirection, embeddingLevels, out var bidi);
234234
var runCount = ICU.GetMethod<ICU.ubidi_countRuns>()(bidi, out var countRunsErrorCode);
235235
ICU.CheckErrorCode<ICU.ubidi_countRuns>(countRunsErrorCode);
236236
_rtl = ICU.GetMethod<ICU.ubidi_getParaLevel>()(bidi) is UBIDI_RTL;
@@ -837,39 +837,63 @@ public void Draw(in Visual.PaintingSession session,
837837
var wordBoundariesIndex = 0;
838838
var highlighterSlices = highlighterSlicer.GetSegments();
839839
var highlighterIndex = 0;
840+
var fallbackHighlight = (background: (CompositionBrush?)null, foreground: (Brush)_blackBrush);
840841
for (var clusterIndex = 0; clusterIndex < _clustersInLogicalOrder.Count; clusterIndex++)
841842
{
842843
var cluster = _clustersInLogicalOrder[clusterIndex];
843-
while (highlighterSlices[highlighterIndex].End <= cluster.Value.start)
844+
RangeSlicer<(CompositionBrush? background, Brush foreground)>.Segment? highlighter = null;
845+
if (highlighterSlices.Count > 0)
844846
{
845-
highlighterIndex++;
846-
}
847+
while (highlighterIndex + 1 < highlighterSlices.Count && highlighterSlices[highlighterIndex].End <= cluster.Value.start)
848+
{
849+
highlighterIndex++;
850+
}
847851

848-
var highlighter = highlighterSlices[highlighterIndex];
852+
highlighter = highlighterSlices[highlighterIndex];
853+
}
849854

850-
while (_runBreaks[runBreakIndex].end <= cluster.Value.start)
855+
if (_runBreaks.Count > 0)
851856
{
852-
runBreakIndex++;
857+
while (runBreakIndex + 1 < _runBreaks.Count && _runBreaks[runBreakIndex].end <= cluster.Value.start)
858+
{
859+
runBreakIndex++;
860+
}
853861
}
854862

855-
while (_wordBoundaries[wordBoundariesIndex] <= cluster.Value.start)
863+
if (_wordBoundaries.Count > 0)
856864
{
857-
wordBoundariesIndex++;
865+
while (wordBoundariesIndex + 1 < _wordBoundaries.Count && _wordBoundaries[wordBoundariesIndex] <= cluster.Value.start)
866+
{
867+
wordBoundariesIndex++;
868+
}
858869
}
859870

860871
var lineIndex = cluster.Value.lineIndex;
872+
if ((uint)lineIndex >= (uint)_lines.Count || (uint)lineIndex >= (uint)_xyTable.Count)
873+
{
874+
continue;
875+
}
876+
877+
var lineMetrics = _xyTable[lineIndex];
878+
if (cluster.Value.indexInLine > 0 && cluster.Value.indexInLine - 1 >= lineMetrics.prefixSummedWidths.Count)
879+
{
880+
continue;
881+
}
882+
861883
var line = _lines[lineIndex];
862-
var y = _xyTable[lineIndex].prefixSummedHeight - line.lineHeight;
884+
var y = lineMetrics.prefixSummedHeight - line.lineHeight;
863885
var unalignedX = cluster.Value.indexInLine == 0
864886
? 0
865-
: _xyTable[lineIndex].prefixSummedWidths[cluster.Value.indexInLine - 1].sumUntilAfterCluster;
887+
: lineMetrics.prefixSummedWidths[cluster.Value.indexInLine - 1].sumUntilAfterCluster;
866888
var alignmentOffset = GetAlignmentOffsetForLine(line);
867889
var positionAcc = new SKPoint(unalignedX + alignmentOffset, y + line.baselineOffset);
868890
var fontDetails = cluster.Value.fontDetails;
869891

870892
if (!cluster.Value.containsTab)
871893
{
872-
var color = BrushToColor(highlighter.Value.foreground is { } h ? h : _runBreaks[runBreakIndex].foreground, session.Opacity);
894+
var runForeground = _runBreaks.Count > 0 ? _runBreaks[runBreakIndex].foreground : _blackBrush;
895+
var highlightValue = highlighter?.Value ?? fallbackHighlight;
896+
var color = BrushToColor(highlightValue.foreground is { } h ? h : runForeground, session.Opacity);
873897
if (!_colorToFontToGlyphs.TryGetValue(color, out var fontToGlyphs))
874898
{
875899
_colorToFontToGlyphs[color] = fontToGlyphs = new Dictionary<SKFont, (List<ushort> glyphs, List<SKPoint> positions)>();
@@ -896,9 +920,9 @@ public void Draw(in Visual.PaintingSession session,
896920
}
897921

898922
var backgroundRect = new SKRect(unalignedX + alignmentOffset, y, unalignedX + alignmentOffset + cluster.Value.width, y + line.lineHeight);
899-
highlighter.Value.background?.Paint(session.Canvas, session.Opacity, backgroundRect);
923+
(highlighter?.Value ?? fallbackHighlight).background?.Paint(session.Canvas, session.Opacity, backgroundRect);
900924

901-
if (_corrections?[wordBoundariesIndex] is { } correction)
925+
if (_corrections is { } corrections && _wordBoundaries.Count > 0 && wordBoundariesIndex < corrections.Count && corrections[wordBoundariesIndex] is { } correction)
902926
{
903927
var correctionIndexBase = wordBoundariesIndex == 0 ? 0 : _wordBoundaries[wordBoundariesIndex - 1];
904928
if (correctionIndexBase + correction.correctionStart <= cluster.Value.start && correctionIndexBase + correction.correctionEnd >= cluster.Value.end)
@@ -1275,10 +1299,79 @@ public int GetIndexAt(Point p, bool ignoreEndingNewLine, bool extendedSelection)
12751299

12761300
public bool IsBaseDirectionRightToLeft => _rtl;
12771301

1302+
private static string SanitizeText(string text)
1303+
{
1304+
var hasInvalid = false;
1305+
for (var i = 0; i < text.Length; i++)
1306+
{
1307+
var c = text[i];
1308+
if (char.IsHighSurrogate(c))
1309+
{
1310+
if (i + 1 < text.Length && char.IsLowSurrogate(text[i + 1]))
1311+
{
1312+
i++;
1313+
continue;
1314+
}
1315+
hasInvalid = true;
1316+
break;
1317+
}
1318+
if (char.IsLowSurrogate(c))
1319+
{
1320+
hasInvalid = true;
1321+
break;
1322+
}
1323+
}
1324+
1325+
if (!hasInvalid)
1326+
{
1327+
return text;
1328+
}
1329+
1330+
var chars = text.ToCharArray();
1331+
for (var i = 0; i < chars.Length; i++)
1332+
{
1333+
var c = chars[i];
1334+
if (char.IsHighSurrogate(c))
1335+
{
1336+
if (i + 1 < chars.Length && char.IsLowSurrogate(chars[i + 1]))
1337+
{
1338+
i++;
1339+
continue;
1340+
}
1341+
chars[i] = '\uFFFD';
1342+
continue;
1343+
}
1344+
if (char.IsLowSurrogate(c))
1345+
{
1346+
chars[i] = '\uFFFD';
1347+
}
1348+
}
1349+
1350+
return new string(chars);
1351+
}
1352+
1353+
private static DisposableStruct<IntPtr> CreateBiDiOrFallback(string text, FlowDirection flowDirection, byte[]? embeddingLevels, out IntPtr bidi)
1354+
{
1355+
var paraLevel = flowDirection is FlowDirection.RightToLeft ? UBIDI_DEFAULT_RTL : UBIDI_DEFAULT_LTR;
1356+
try
1357+
{
1358+
return ICU.CreateBiDiAndSetPara(text, 0, text.Length, paraLevel, out bidi, embeddingLevels);
1359+
}
1360+
catch (InvalidOperationException ex) when (embeddingLevels is not null)
1361+
{
1362+
typeof(UnicodeText).LogError()?.Error("ubidi_setPara failed with embedding levels; retrying without them.", ex);
1363+
return ICU.CreateBiDiAndSetPara(text, 0, text.Length, paraLevel, out bidi, embeddingLevels: null);
1364+
}
1365+
}
1366+
12781367
private static List<int> GetWords(string text)
12791368
{
12801369
var boundaries = new List<int>();
12811370
AppendBoundaries(/* Word */ 1, text, 0, boundaries);
1371+
if (boundaries.Count == 0)
1372+
{
1373+
return new List<int> { text.Length };
1374+
}
12821375
var ret = new List<int> { boundaries[0] };
12831376
for (var index = 1; index < boundaries.Count; index++)
12841377
{
@@ -1294,6 +1387,10 @@ private static List<int> GetWords(string text)
12941387
}
12951388
ret.Add(boundary);
12961389
}
1390+
if (ret[^1] != text.Length)
1391+
{
1392+
ret.Add(text.Length);
1393+
}
12971394

12981395
return ret;
12991396
}

0 commit comments

Comments
 (0)