@@ -140,7 +140,10 @@ public class MojibusterEncodingDetector implements EncodingDetector {
140140 * genuine false-CJK ≥5.3%, so ~2.5% separates them (see CjkDecodeValidator). */
141141 private static final double CJK_FAILURE_VETO_THRESHOLD = 0.025 ;
142142
143- /** Confidence for the windows-1252 fallback emitted on empty/ASCII probes. */
143+ /** Confidence for the windows-1252 fallback emitted on empty/ASCII probes.
144+ * MUST equal JunkFilterEncodingDetector.NO_INFO_CONFIDENCE (tika-ml-junkdetect):
145+ * its strict {@code confidence > NO_INFO_CONFIDENCE} test treats exactly this
146+ * value as "statistical layer abstained" so a declaration can win. */
144147 private static final float FALLBACK_CONFIDENCE = 0.1f ;
145148
146149 /**
@@ -335,15 +338,16 @@ public List<EncodingResult> detect(byte[] probe, Metadata metadata) {
335338 // because STRUCTURAL confidence outranks STATISTICAL.
336339 // • AMBIGUOUS (pure ASCII or only truncated lead): no
337340 // emission; NB + fallbacks handle it.
338- StructuralEncodingRules .Utf8Result utf8 = StructuralEncodingRules .checkUtf8 (probe );
341+ StructuralEncodingRules .Utf8Stats utf8Stats = StructuralEncodingRules .utf8Stats (probe );
342+ StructuralEncodingRules .Utf8Result utf8 = utf8Stats .toResult ();
339343 // TACTICAL: tolerate small corruption. If the grammar check returned
340344 // NOT_UTF8 but the malformed-byte fraction is tiny, treat as UTF-8 —
341345 // a single bad continuation byte in 2KB of CJK is nearly always
342346 // corruption, not "this isn't UTF-8". Remove when grammar check is
343347 // replaced with a probabilistic decoder.
344348 boolean utf8Tolerated = false ;
345349 if (utf8 == StructuralEncodingRules .Utf8Result .NOT_UTF8 ) {
346- int errors = StructuralEncodingRules . countUtf8Errors ( probe );
350+ int errors = utf8Stats . errors ( );
347351 // Length-aware: absolute floor for short probes, rate for long ones.
348352 int maxTolerated = Math .max (UTF8_MAX_TOLERATED_ERRORS ,
349353 (int ) (probe .length * UTF8_MALFORMED_TOLERANCE ));
@@ -371,7 +375,7 @@ public List<EncodingResult> detect(byte[] probe, Metadata metadata) {
371375 // document its STRUCTURAL proof and JunkFilter has nothing to prefer
372376 // over the declared charset.
373377 boolean evidenceTolerated = utf8Tolerated
374- && StructuralEncodingRules . countUtf8Sequences ( probe ) >= MIN_TOLERATED_UTF8_SEQUENCES ;
378+ && utf8Stats . sequences ( ) >= MIN_TOLERATED_UTF8_SEQUENCES ;
375379 if (utf8 == StructuralEncodingRules .Utf8Result .LIKELY_UTF8 || evidenceTolerated ) {
376380 pool .add (new EncodingResult (
377381 java .nio .charset .StandardCharsets .UTF_8 ,
@@ -534,83 +538,6 @@ private static boolean isPureAscii(byte[] probe) {
534538 return true ;
535539 }
536540
537- /**
538- * Resolve UTF-16 to LE or BE once NB has called it "UTF-16".
539- *
540- * <p>Two deterministic tests:
541- * <ol>
542- * <li>Null-density: count null bytes in even-offset positions
543- * vs odd-offset positions. For ASCII-in-UTF-16-LE the
544- * high byte is 0x00 at odd positions; for BE it's at even
545- * positions. If one column is clearly null-dominant, that
546- * column indicates the endianness.</li>
547- * <li>Codepoint validity fallback: for ambiguous probes (pure
548- * CJK UTF-16, no nulls in either column) count how many
549- * 16-bit codepoints under LE vs BE interpretation land in
550- * assigned Unicode BMP ranges (non-PUA, non-unassigned).
551- * Whichever interpretation yields more valid codepoints
552- * wins.</li>
553- * </ol>
554- *
555- * <p>Also honors the {@code invalidUtf16Le}/{@code invalidUtf16Be}
556- * flags from {@link WideUnicodeDetector} — if either endianness
557- * is structurally invalid (surrogate-pair violation), the other
558- * wins by default.
559- *
560- * @return the resolved charset, or {@code null} if the probe is
561- * structurally invalid under both interpretations
562- */
563- private static java .nio .charset .Charset disambiguateUtf16 (byte [] probe ,
564- boolean invalidLe ,
565- boolean invalidBe ) {
566- if (invalidLe && invalidBe ) {
567- return null ;
568- }
569- if (invalidLe ) {
570- return java .nio .charset .Charset .forName ("UTF-16BE" );
571- }
572- if (invalidBe ) {
573- return java .nio .charset .Charset .forName ("UTF-16LE" );
574- }
575- int nullEven = 0 ;
576- int nullOdd = 0 ;
577- for (int i = 0 ; i + 1 < probe .length ; i += 2 ) {
578- if (probe [i ] == 0 ) nullEven ++;
579- if (probe [i + 1 ] == 0 ) nullOdd ++;
580- }
581- // Clear null-density winner: one column is ≥ 3× more
582- // null-dominant than the other.
583- if (nullEven >= 3 * Math .max (1 , nullOdd )) {
584- return java .nio .charset .Charset .forName ("UTF-16BE" );
585- }
586- if (nullOdd >= 3 * Math .max (1 , nullEven )) {
587- return java .nio .charset .Charset .forName ("UTF-16LE" );
588- }
589- // Ambiguous on null-density (CJK content). Count valid BMP
590- // codepoints under each interpretation. A "valid" codepoint
591- // is any non-zero codepoint outside the surrogate range
592- // (0xD800-0xDFFF) — for CJK content most bytes map into
593- // assigned blocks, and random-byte-interpreted-as-UTF-16
594- // produces many surrogate-range halves.
595- int validLe = 0 ;
596- int validBe = 0 ;
597- for (int i = 0 ; i + 1 < probe .length ; i += 2 ) {
598- int lo = probe [i ] & 0xFF ;
599- int hi = probe [i + 1 ] & 0xFF ;
600- int leCp = (hi << 8 ) | lo ;
601- int beCp = (lo << 8 ) | hi ;
602- if (leCp != 0 && (leCp < 0xD800 || leCp > 0xDFFF )) {
603- validLe ++;
604- }
605- if (beCp != 0 && (beCp < 0xD800 || beCp > 0xDFFF )) {
606- validBe ++;
607- }
608- }
609- return validLe >= validBe
610- ? java .nio .charset .Charset .forName ("UTF-16LE" )
611- : java .nio .charset .Charset .forName ("UTF-16BE" );
612- }
613-
614541 /**
615542 * Relabel the top result to windows-1252 when top is a non-1252
616543 * member of {@link CharsetConfusables#SBCS_LATIN_FAMILY} and
0 commit comments