Skip to content

Commit f33e0c9

Browse files
committed
Add fallback version of UTF8toUTF16
1 parent 46a5459 commit f33e0c9

3 files changed

Lines changed: 113 additions & 6 deletions

File tree

src/Lucene.Net.Tests/Util/TestUnicodeUtil.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,5 +339,17 @@ public void TestTryUTF8toUTF16()
339339
Assert.IsTrue(success);
340340
Assert.AreEqual(unicode, chars?.ToString());
341341
}
342+
343+
[Test]
344+
[LuceneNetSpecific] // this is a Lucene.NET specific method
345+
public void TestUTF8toUTF16WithFallback()
346+
{
347+
byte[] invalidUtf8 = { 0x63, 0xc3 }; // Invalid ending UTF-8 sequence
348+
var scratch = new CharsRef();
349+
350+
UnicodeUtil.UTF8toUTF16WithFallback(invalidUtf8, scratch);
351+
352+
Assert.AreEqual("c\ufffd", scratch.ToString());
353+
}
342354
}
343355
}

src/Lucene.Net/Util/BytesRef.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,23 @@ public string Utf8ToString()
243243
return @ref.ToString();
244244
}
245245

246+
/// <summary>
247+
/// Interprets stored bytes as UTF8 bytes, returning the
248+
/// resulting <see cref="string"/>.
249+
/// </summary>
250+
/// <remarks>
251+
/// LUCENENET specific version that does not throw exceptions,
252+
/// primarily for use in ToString() and other methods that
253+
/// should not throw exceptions.
254+
/// </remarks>
255+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
256+
public string Utf8ToStringWithFallback()
257+
{
258+
CharsRef @ref = new CharsRef(Length);
259+
UnicodeUtil.UTF8toUTF16WithFallback(bytes, Offset, Length, @ref);
260+
return @ref.ToString();
261+
}
262+
246263
#nullable enable
247264
/// <summary>
248265
/// Tries to interpret the stored bytes as UTF8 bytes, returning the

src/Lucene.Net/Util/UnicodeUtil.cs

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,90 @@ public static void UTF8toUTF16(ReadOnlySpan<byte> utf8, CharsRef chars)
952952
chars.Length = out_offset - chars.Offset;
953953
}
954954

955+
/// <summary>
956+
/// Interprets the given byte array as UTF-8 and converts to UTF-16. The <see cref="CharsRef"/> will be extended if
957+
/// it doesn't provide enough space to hold the worst case of each byte becoming a UTF-16 codepoint.
958+
/// <para/>
959+
/// NOTE: This method will replace any invalid UTF-8 byte sequences with the Unicode replacement character U+FFFD.
960+
/// </summary>
961+
/// <remarks>
962+
/// LUCENENET specific, for use in ToString() where we want to avoid throwing exceptions.
963+
/// </remarks>
964+
/// <seealso cref="UTF8toUTF16WithFallback(ReadOnlySpan{byte}, CharsRef)"/>
965+
// TODO: broken if chars.offset != 0
966+
public static void UTF8toUTF16WithFallback(byte[] utf8, int offset, int length, CharsRef chars)
967+
{
968+
UTF8toUTF16(utf8.AsSpan(offset, length), chars);
969+
}
970+
971+
/// <summary>
972+
/// Interprets the given byte span as UTF-8 and converts to UTF-16. The <see cref="CharsRef"/> will be extended if
973+
/// it doesn't provide enough space to hold the worst case of each byte becoming a UTF-16 codepoint.
974+
/// <para/>
975+
/// NOTE: This method will replace any invalid UTF-8 byte sequences with the Unicode replacement character U+FFFD.
976+
/// </summary>
977+
/// <remarks>
978+
/// LUCENENET specific, for use in ToString() where we want to avoid throwing exceptions.
979+
/// </remarks>
980+
// TODO: broken if chars.offset != 0
981+
public static void UTF8toUTF16WithFallback(ReadOnlySpan<byte> utf8, CharsRef chars)
982+
{
983+
int out_offset = chars.Offset = 0;
984+
char[] @out = chars.Chars = ArrayUtil.Grow(chars.Chars, utf8.Length);
985+
int i = 0;
986+
987+
while (i < utf8.Length)
988+
{
989+
int b = utf8[i++] & 0xff;
990+
if (b < 0xc0)
991+
{
992+
if (Debugging.AssertsEnabled) Debugging.Assert(b < 0x80);
993+
@out[out_offset++] = (char)b;
994+
}
995+
else if (b < 0xe0)
996+
{
997+
if (utf8.Length <= i)
998+
{
999+
@out[out_offset++] = (char)0xfffd;
1000+
continue;
1001+
}
1002+
@out[out_offset++] = (char)(((b & 0x1f) << 6) + (utf8[i++] & 0x3f));
1003+
}
1004+
else if (b < 0xf0)
1005+
{
1006+
if (utf8.Length <= i + 1)
1007+
{
1008+
@out[out_offset++] = (char)0xfffd;
1009+
break;
1010+
}
1011+
@out[out_offset++] = (char)(((b & 0xf) << 12) + ((utf8[i] & 0x3f) << 6) + (utf8[i + 1] & 0x3f));
1012+
i += 2;
1013+
}
1014+
else
1015+
{
1016+
if (utf8.Length <= i + 2)
1017+
{
1018+
@out[out_offset++] = (char)0xfffd;
1019+
break;
1020+
}
1021+
if (Debugging.AssertsEnabled) Debugging.Assert(b < 0xf8, "b = 0x{0:x}", b);
1022+
int ch = ((b & 0x7) << 18) + ((utf8[i] & 0x3f) << 12) + ((utf8[i + 1] & 0x3f) << 6) + (utf8[i + 2] & 0x3f);
1023+
i += 3;
1024+
if (ch < UNI_MAX_BMP)
1025+
{
1026+
@out[out_offset++] = (char)ch;
1027+
}
1028+
else
1029+
{
1030+
int chHalf = ch - 0x0010000;
1031+
@out[out_offset++] = (char)((chHalf >> 10) + 0xD800);
1032+
@out[out_offset++] = (char)((chHalf & HALF_MASK) + 0xDC00);
1033+
}
1034+
}
1035+
}
1036+
chars.Length = out_offset - chars.Offset;
1037+
}
1038+
9551039
/// <summary>
9561040
/// Tries to interpret the given byte span as UTF-8 and convert to UTF-16, providing the result in a new <see cref="CharsRef"/>.
9571041
/// <para/>
@@ -983,12 +1067,6 @@ public static bool TryUTF8toUTF16(ReadOnlySpan<byte> utf8, [NotNullWhen(true)] o
9831067

9841068
while (i < utf8.Length)
9851069
{
986-
if (utf8.Length <= i)
987-
{
988-
chars = null;
989-
return false;
990-
}
991-
9921070
int b = utf8[i++] & 0xff;
9931071
if (b < 0xc0)
9941072
{

0 commit comments

Comments
 (0)