Skip to content

Commit b0900ac

Browse files
committed
TIKA-4810 -- follow-on: single-pass Utf8Stats walker + loose-end cleanup
1 parent d4091f5 commit b0900ac

7 files changed

Lines changed: 251 additions & 315 deletions

File tree

.skills/tika-eval-compare.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Ask the user for:
2222
zip archive containing `tika-app-*.jar`, `lib/`, and `plugins/`.
2323
- A corpus of input files (a directory tree).
2424
- tika-eval-app, built from `tika-eval/tika-eval-app` (use the zip).
25+
The bare `target/tika-eval-app-*.jar` is thin (no bundled deps) and dies with
26+
`NoClassDefFoundError: ...GzipCompressorOutputStream` (esp. with `-r`). Always
27+
run the jar from the unzipped `target/*.zip` dir, which carries its `lib/`.
2528
- **Enable MD5 digesting** in both configs so tika-eval can match
2629
embedded documents by content hash (not just index position).
2730
Add to the config JSON:

tika-encoding-detectors/tika-encoding-detector-mojibuster/src/main/java/org/apache/tika/ml/chardetect/CjkDecodeValidator.java

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public static double strippedFailureRate(byte[] bytes, Charset cjkCharset) {
9494
i++;
9595
continue;
9696
}
97-
int ulen = utf8SequenceLength(bytes, i);
97+
int ulen = StructuralEncodingRules.utf8SequenceLength(bytes, i);
9898
if (ulen > 0) {
9999
nUtf8Seqs++;
100100
i += ulen; // embedded UTF-8 — not legacy content, skip
@@ -136,30 +136,4 @@ public static boolean appliesTo(String charsetName) {
136136
|| name.contains("shift") || name.contains("jis") || name.contains("949");
137137
}
138138

139-
/** Length (2/3/4) of a valid UTF-8 multi-byte sequence starting at {@code i},
140-
* or 0 if none. Lead-byte ranges exclude overlong 2-byte (C0/C1) and
141-
* out-of-range (≥F5) leads; continuations must be 0x80–0xBF. */
142-
static int utf8SequenceLength(byte[] b, int i) {
143-
int x = b[i] & 0xFF;
144-
int len;
145-
if (x >= 0xC2 && x <= 0xDF) {
146-
len = 2;
147-
} else if (x >= 0xE0 && x <= 0xEF) {
148-
len = 3;
149-
} else if (x >= 0xF0 && x <= 0xF4) {
150-
len = 4;
151-
} else {
152-
return 0;
153-
}
154-
if (i + len > b.length) {
155-
return 0;
156-
}
157-
for (int k = 1; k < len; k++) {
158-
int c = b[i + k] & 0xFF;
159-
if (c < 0x80 || c > 0xBF) {
160-
return 0;
161-
}
162-
}
163-
return len;
164-
}
165139
}

tika-encoding-detectors/tika-encoding-detector-mojibuster/src/main/java/org/apache/tika/ml/chardetect/MojibusterEncodingDetector.java

Lines changed: 8 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)