Skip to content

Commit 5e06d71

Browse files
committed
fix(text): harden UnicodeText surrogate, indexing, and ICU bidi fallback
- Add defensive draw-time bounds checks for highlighter/run-break/word-boundary and line metric indexing. - Sanitize malformed UTF-16 surrogate units to U+FFFD before shaping. - Retry ICU ubidi_setPara without embedding levels when explicit levels fail, with bidi handle cleanup. - Keep/update unit and Skia runtime regression coverage for malformed surrogate and highlighter-range scenarios.
1 parent 9c7f669 commit 5e06d71

File tree

4 files changed

+280
-34
lines changed

4 files changed

+280
-34
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
using Microsoft.UI.Xaml;
88
using Microsoft.UI.Xaml.Documents;
99
using Microsoft.UI.Xaml.Controls;
10-
using System.Drawing;
1110
using Microsoft.UI.Xaml.Media;
11+
using Windows.Foundation;
1212

1313
namespace Uno.UI.Tests.Windows_UI_XAML_Controls.TextBlockTests
1414
{
@@ -51,6 +51,55 @@ public void When_Changing_Foreground_Property()
5151
}
5252
#endif
5353

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

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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,24 +176,32 @@ public static T GetMethod<T>()
176176
public static unsafe DisposableStruct<IntPtr> CreateBiDiAndSetPara(string text, int start, int end, byte paraLevel, out IntPtr bidi, byte[]? embeddingLevels = null)
177177
{
178178
bidi = GetMethod<ubidi_open>()();
179-
fixed (char* textPtr = &text.GetPinnableReference())
179+
try
180180
{
181-
int setParaErrorCode;
182-
if (embeddingLevels is not null)
181+
fixed (char* textPtr = &text.GetPinnableReference())
183182
{
184-
fixed (byte* embeddingLevelsPtr = embeddingLevels)
183+
int setParaErrorCode;
184+
if (embeddingLevels is not null)
185185
{
186-
GetMethod<ubidi_setPara>()(bidi, (IntPtr)(textPtr + start), end - start, paraLevel, (IntPtr)embeddingLevelsPtr, out setParaErrorCode);
186+
fixed (byte* embeddingLevelsPtr = embeddingLevels)
187+
{
188+
GetMethod<ubidi_setPara>()(bidi, (IntPtr)(textPtr + start), end - start, paraLevel, (IntPtr)embeddingLevelsPtr, out setParaErrorCode);
189+
}
190+
}
191+
else
192+
{
193+
GetMethod<ubidi_setPara>()(bidi, (IntPtr)(textPtr + start), end - start, paraLevel, IntPtr.Zero, out setParaErrorCode);
194+
}
195+
if (setParaErrorCode > 0)
196+
{
197+
throw new InvalidOperationException($"{nameof(ubidi_setPara)} failed with error code {setParaErrorCode}");
187198
}
188199
}
189-
else
190-
{
191-
GetMethod<ubidi_setPara>()(bidi, (IntPtr)(textPtr + start), end - start, paraLevel, IntPtr.Zero, out setParaErrorCode);
192-
}
193-
if (setParaErrorCode > 0)
194-
{
195-
throw new InvalidOperationException($"{nameof(ubidi_setPara)} failed with error code {setParaErrorCode}");
196-
}
200+
}
201+
catch
202+
{
203+
GetMethod<ubidi_close>()(bidi);
204+
throw;
197205
}
198206
return new DisposableStruct<IntPtr>(static bidi => GetMethod<ubidi_close>()(bidi), bidi);
199207
}

0 commit comments

Comments
 (0)