Skip to content

Commit 0521bba

Browse files
paulirwinCopilot
andauthored
UnicodeUtil updates: TryUTF8toUTF16, ReadOnlySpan methods, #1024 (#1057)
* UnicodeUtil updates: TryUTF8toUTF16, ReadOnlySpan methods, #1024 * Add back array overloads; add unit test for TryUTF8toUTF16 * Fix comment formatting * Remove offset/length parameters from Span-based methods, #1024 * Move ToCharArray methods to ObsoleteAPI for removal in 4.8.0 RC, #1024 * Add fallback version of UTF8toUTF16 * Throw ParseException on out of range in UTF8toUTF16, add more tests * Use Utf8ToStringWithFallback in ToString and exception/logging message building * Use FormatException instead of ParseException * Add FormatException case to Utf8ToString() catch * Fix exception type in XML docs in UnicodeUtil.NewString Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix TestUnicodeUtil: Use IOUtils.ENCODING_UTF_8_NO_BOM instead of non-existent CHARSET_UTF_8 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c4d6fe3 commit 0521bba

14 files changed

Lines changed: 622 additions & 200 deletions

File tree

src/Lucene.Net.Codecs/SimpleText/SimpleTextDocValuesReader.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,10 +411,10 @@ public override int GetOrd(int docId)
411411
SimpleTextUtil.ReadLine(_input, _scratch);
412412
try
413413
{
414-
// LUCNENENET: .NET doesn't have a way to specify a pattern with integer, but all of the standard ones are built in.
414+
// LUCENENET: .NET doesn't have a way to specify a pattern with integer, but all of the standard ones are built in.
415415
return int.Parse(_scratch.Utf8ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture) - 1;
416416
}
417-
catch (Exception pe) when (pe.IsParseException())
417+
catch (Exception pe) when (pe.IsParseException() || pe.IsNumberFormatException())
418418
{
419419
var e = new CorruptIndexException($"failed to parse ord (resource={_input})", pe);
420420
throw e;

src/Lucene.Net.Codecs/SimpleText/SimpleTextUtil.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ public static void CheckFooter(ChecksumIndexInput input)
106106

107107
if (StringHelper.StartsWith(scratch, CHECKSUM) == false)
108108
{
109+
// LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
109110
throw new CorruptIndexException("SimpleText failure: expected checksum line but got " +
110-
scratch.Utf8ToString() + " (resource=" + input + ")");
111+
scratch.Utf8ToStringWithFallback() + " (resource=" + input + ")");
111112
}
112113
var actualChecksum =
113114
(new BytesRef(scratch.Bytes, CHECKSUM.Length, scratch.Length - CHECKSUM.Length)).Utf8ToString();

src/Lucene.Net.Facet/SortedSet/DefaultSortedSetDocValuesReaderState.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ public DefaultSortedSetDocValuesReaderState(IndexReader reader, string field = F
7979
string[] components = FacetsConfig.StringToPath(spare.Utf8ToString());
8080
if (components.Length != 2)
8181
{
82-
throw new ArgumentException("this class can only handle 2 level hierarchy (dim/value); got: " + Arrays.ToString(components) + " " + spare.Utf8ToString());
82+
// LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
83+
throw new ArgumentException("this class can only handle 2 level hierarchy (dim/value); got: " + Arrays.ToString(components) + " " + spare.Utf8ToStringWithFallback());
8384
}
8485
if (!components[0].Equals(lastDim, StringComparison.Ordinal))
8586
{

src/Lucene.Net.Grouping/AbstractGroupFacetCollector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ public override int GetHashCode()
278278
public override string ToString()
279279
{
280280
return "FacetEntry{" +
281-
"value=" + value.Utf8ToString() +
281+
"value=" + value.Utf8ToStringWithFallback() + // LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
282282
", count=" + count +
283283
'}';
284284
}

src/Lucene.Net.Join/TermsIncludingScoreQuery.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ internal Explanation Explain(int target) // LUCENENET NOTE: changed accessibilit
314314
} while (docId != DocIdSetIterator.NO_MORE_DOCS);
315315

316316
return new ComplexExplanation(true, outerInstance._scores[outerInstance._ords[_scoreUpto]],
317-
"Score based on join value " + _termsEnum.Term.Utf8ToString());
317+
"Score based on join value " + _termsEnum.Term.Utf8ToStringWithFallback()); // LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
318318
}
319319
}
320320

src/Lucene.Net.Misc/Misc/TermStats.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ internal string GetTermText()
4545

4646
public override string ToString()
4747
{
48-
return ("TermStats: Term=" + TermText.Utf8ToString() + " DocFreq=" + DocFreq + " TotalTermFreq=" + TotalTermFreq);
48+
// LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
49+
return "TermStats: Term=" + TermText.Utf8ToStringWithFallback() + " DocFreq=" + DocFreq + " TotalTermFreq=" + TotalTermFreq;
4950
}
5051
}
5152
}

src/Lucene.Net.Queries/TermsFilter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ public override string ToString()
319319
}
320320
first = false;
321321
builder.Append(current.field).Append(':');
322-
builder.Append(spare.Utf8ToString());
322+
builder.Append(spare.Utf8ToStringWithFallback()); // LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
323323
}
324324
}
325325

src/Lucene.Net.Suggest/Suggest/Fst/FSTCompletion.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ internal Completion(BytesRef key, int bucket)
5959

6060
public override string ToString()
6161
{
62-
return Utf8.Utf8ToString() + "/" + Bucket.ToString("0.0", CultureInfo.InvariantCulture);
62+
// LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
63+
return Utf8.Utf8ToStringWithFallback() + "/" + Bucket.ToString("0.0", CultureInfo.InvariantCulture);
6364
}
6465

6566
/// <seealso cref="BytesRef.CompareTo(object)"></seealso>

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,5 +326,54 @@ public virtual void TestUTF8UTF16CharsRef()
326326
Assert.AreEqual(cRef.ToString(), unicode);
327327
}
328328
}
329+
330+
[Test]
331+
[LuceneNetSpecific]
332+
[TestCase(new byte[] { 0x63, 0x61, 0xc3 }, true)] // ca�, start of 2-byte sequence
333+
[TestCase(new byte[] { 0x63, 0x61, 0xe3 }, true)] // ca�, start of 3-byte sequence
334+
[TestCase(new byte[] { 0x63, 0x61, 0xf3 }, true)] // ca�, start of 4-byte sequence
335+
[TestCase(new byte[] { 0x63, 0x61, 0xc3, 0xb1, 0x6f, 0x6e }, false)] // cañon
336+
public void TestUTF8toUTF16Exception(byte[] invalidUtf8, bool shouldThrow)
337+
{
338+
var scratch = new CharsRef();
339+
340+
if (shouldThrow)
341+
{
342+
Assert.Throws<FormatException>(() => UnicodeUtil.UTF8toUTF16(invalidUtf8, scratch));
343+
}
344+
else
345+
{
346+
UnicodeUtil.UTF8toUTF16(invalidUtf8, scratch);
347+
}
348+
}
349+
350+
[Test]
351+
[LuceneNetSpecific] // this is a Lucene.NET specific method
352+
[Repeat(100)]
353+
public void TestTryUTF8toUTF16()
354+
{
355+
string unicode = TestUtil.RandomRealisticUnicodeString(Random);
356+
var utf8 = new BytesRef(IOUtils.ENCODING_UTF_8_NO_BOM.GetBytes(unicode));
357+
358+
bool success = UnicodeUtil.TryUTF8toUTF16(utf8, out var chars);
359+
360+
Assert.IsTrue(success);
361+
Assert.AreEqual(unicode, chars?.ToString());
362+
}
363+
364+
[Test]
365+
[LuceneNetSpecific] // this is a Lucene.NET specific method
366+
[TestCase(new byte[] { 0x63, 0x61, 0xc3 }, "ca\ufffd")] // ca�, start of 2-byte sequence
367+
[TestCase(new byte[] { 0x63, 0x61, 0xe3 }, "ca\ufffd")] // ca�, start of 3-byte sequence
368+
[TestCase(new byte[] { 0x63, 0x61, 0xf3 }, "ca\ufffd")] // ca�, start of 4-byte sequence
369+
[TestCase(new byte[] { 0x63, 0x61, 0xc3, 0xb1, 0x6f, 0x6e }, "cañon")]
370+
public void TestUTF8toUTF16WithFallback(byte[] utf8, string expected)
371+
{
372+
var scratch = new CharsRef();
373+
374+
UnicodeUtil.UTF8toUTF16WithFallback(utf8, scratch);
375+
376+
Assert.AreEqual(expected, scratch.ToString());
377+
}
329378
}
330379
}

src/Lucene.Net/Codecs/BlockTreeTermsWriter.cs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,8 @@ public PendingTerm(BytesRef term, BlockTermState state)
440440

441441
public override string ToString()
442442
{
443-
return Term.Utf8ToString();
443+
// LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
444+
return Term.Utf8ToStringWithFallback();
444445
}
445446
}
446447

@@ -468,20 +469,33 @@ public PendingBlock(BytesRef prefix, long fp, bool hasTerms, bool isFloor, int f
468469

469470
public override string ToString()
470471
{
471-
return "BLOCK: " + Prefix.Utf8ToString();
472+
// LUCENENET specific - use Utf8ToStringWithFallback() to handle invalid UTF-8 bytes
473+
return $"BLOCK: {Prefix.Utf8ToStringWithFallback()}";
474+
}
475+
476+
#nullable enable
477+
public bool TryToString([NotNullWhen(true)] out string? result)
478+
{
479+
if (Prefix.TryUtf8ToString(out string? prefixString))
480+
{
481+
result = $"BLOCK: {prefixString}";
482+
return true;
483+
}
484+
485+
result = null;
486+
return false;
472487
}
473488

474489
// LUCENENET specific - to keep the Debug.Assert statement from throwing exceptions
475490
// because of invalid UTF8 code in Prefix, we have a wrapper class that falls back
476491
// to using PendingBlock.Prefix.ToString() if PendingBlock.ToString() errors.
477492
// This struct defers formatting the string until it is actually used as a parameter
478493
// in string.Format().
479-
private struct PendingBlocksFormatter // For assert
494+
private readonly struct PendingBlocksFormatter // For assert
480495
{
481-
#pragma warning disable IDE0044 // Add readonly modifier
482-
private IList<PendingBlock> blocks;
483-
#pragma warning restore IDE0044 // Add readonly modifier
484-
public PendingBlocksFormatter(IList<PendingBlock> blocks)
496+
private readonly IList<PendingBlock>? blocks;
497+
498+
public PendingBlocksFormatter(IList<PendingBlock>? blocks)
485499
{
486500
this.blocks = blocks; // May be null
487501
}
@@ -500,17 +514,17 @@ public override string ToString() // For assert
500514
it.MoveNext();
501515
while (true)
502516
{
503-
var e = it.Current;
517+
var e = it.Current ?? throw new InvalidOperationException("Expected a non-null value in the enumerator due to Count check above.");
504518
// There is a chance that the Prefix will contain invalid UTF8,
505519
// so we catch that and use the alternative way of displaying it
506-
try
520+
if (e.TryToString(out string? eString))
507521
{
508-
sb.Append(e.ToString());
522+
sb.Append(eString);
509523
}
510-
catch (IndexOutOfRangeException)
524+
else
511525
{
512526
sb.Append("BLOCK: ");
513-
sb.Append(e.Prefix.ToString());
527+
sb.Append(e.Prefix);
514528
}
515529
if (!it.MoveNext())
516530
{
@@ -520,6 +534,7 @@ public override string ToString() // For assert
520534
}
521535
}
522536
}
537+
#nullable restore
523538

524539
public void CompileIndex(IList<PendingBlock> floorBlocks, RAMOutputStream scratchBytes)
525540
{

0 commit comments

Comments
 (0)