All findings · Lucene finding · LFV-719 · icutokenizerfactory-rulefile-utf8-bom-kept

ICUTokenizerFactory keeps the UTF-8 byte order mark of a custom rule file as rule text

An ICUTokenizerFactory rule file saved with a UTF-8 byte order mark has the invisible mark glued to its first rule: with a BOM, Lucene's KeywordTokenizer.rbbi silently loses its Cyrillic and Thai tokens (4 of 6 left). Files starting with a !! directive fail with a misleading syntax error.

Summary

Priority
Low, rank 503This is the same kept-BOM slip in ICUTokenizerFactory's opt-in custom rule files, where most such files fail loudly and only a file that starts with a rule silently loses tokens.
Impact
silent wrong results
Since
4.1.0 (SOLR-4123, 2012)introduced 2012-12-03 · over 13 years
Affected
4.1.0 to 10.6.0 (10.6.0 by reading) and main, with custom rulefiles only
Status
documented
Found by
bounded enumeration of rule-file variants through the tokenizer

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

A custom ICU rule file saved with a UTF-8 byte order mark: the mark ends up inside the first rule.

Diagram of a custom ICU rule file that starts with a UTF-8 byte order mark: Lucene keeps the mark as U+FEFF in front of the first rule, so that rule no longer matches and the tokenizer emits no tokens for its text.
How the 3 invisible bytes at the start of a custom rule file end up inside the first ICU rule, and what the tokenizer then emits. Select the still to open it at full size. Generated with AI, checked against the code.
  1. You tailor one script with your own rule file: rulefiles="Latn:keyword.rbbi", where the file holds the single rule .+ {200}; (all Latin text is one token). Your editor saves it as UTF-8 with a byte order mark (BOM): 3 invisible bytes, EF BB BF, at the start. ICU's own rule-file compiler, genbrk, accepts and recommends that.
  2. You expect the same tokens as from the file without the BOM: ab cd gives one token, [ab cd].
  3. You get 0 tokens and no error. Lucene decodes the BOM to the character U+FEFF and hands it to ICU as the first character of the first rule, so that rule only matches text that starts with U+FEFF. The tokenizer drops every piece of text with rule status 0, which is what text that no rule matched gets.
  4. Lucene's own test file KeywordTokenizer.rbbi (comment lines, then .+ {200};) with a BOM loses the Cyrillic and Thai tokens of its upstream test: 4 tokens instead of 6. With a second rule after the first, only the first rule's tokens vanish: ab 12 cd gives [12] instead of [ab] [12] [cd].
  5. A file that starts with a !! directive fails instead: Lucene's Latin-break-only-on-whitespace.rbbi with a BOM throws IllegalArgumentException: Error 66052 at line 20 column 1, an ICU syntax error that points at line 20, while the cause sits invisibly on line 1.

Who is affected

Only applications that set rulefiles (custom per-script ICU rules) and save a rule file as UTF-8 with a BOM, as editors such as Windows Notepad often do. The default configuration is unaffected. A file that starts with a rule silently loses that rule's tokens; one that starts with a directive fails at setup. Rare: no report found since 4.1.0.

Mechanism, terms and numbers745 words

ICUTokenizerFactory's rulefiles argument replaces the break rules of one script with a rule file of your own. parseRules reads that file as UTF-8, blanks the lines that start with # and passes the text to ICU's new RuleBasedBreakIterator(String), but it never removes a leading byte order mark. ICU's string API treats U+FEFF as an ordinary literal character, so the BOM becomes part of the file's first statement. If that statement is a rule, it compiles but can no longer match: the one-rule file .+ {200}; gives no token for ab cd, and Lucene's own test file KeywordTokenizer.rbbi with a BOM drops the Cyrillic and Thai tokens (4 tokens instead of 6), with no error. If it is a !! directive, ICU throws Error 66052 at line 20 column 1 (a rule syntax error) for Lucene's Latin-break-only-on-whitespace.rbbi, pointing away from the invisible cause on line 1. ICU's own compiler for this file format, genbrk, strips a BOM and its man page recommends one; Lucene's word-list and hunspell readers skip it too. Present since rulefiles arrived in 4.1.0 (SOLR-4123, 2012). Skipping a U+FEFF at the start of the first line fixes it; the whole ICU module passes with that change.

Terms

  • rulefiles: an argument of ICUTokenizerFactory (ICU analysis module) that replaces the break rules of one script with your own ICU rule file, for example rulefiles="Latn:my.Latin.rules.rbbi".
  • Rule status: each ICU rule ends with a number such as {200}; text a rule matches gets that status. ICUTokenizer emits only pieces with a non-zero status and skips status-0 pieces (ICUTokenizer.java:209-213).
  • Byte order mark (BOM): the 3 bytes EF BB BF that many editors write at the start of a UTF-8 file. Decoded, it is the character U+FEFF.

The bug

  1. inform calls parseRules once per script entry of the rulefiles map (ICUTokenizerFactory.java:108-135, the call at :119; a repeated script overwrites the earlier pair, :86-95).
  2. parseRules reads the file with IOUtils.getDecodingReader(stream, UTF_8) (:141). Java's UTF-8 decoder keeps a leading BOM as U+FEFF.
  3. line.startsWith("#") is false for U+FEFF# ..., so the negated test if (!line.startsWith("#")) (:144) is true and the first line is kept instead of blanked. Lines are joined with \n, so line numbers stay as in the file (:147).
  4. new RuleBasedBreakIterator(rules.toString()) (:150). ICU's rule scanner skips only Pattern_White_Space; U+FEFF is not in that set, so it is a literal character. ICU still reads the rest of a # line as a comment, so U+FEFF joins the first statement, after any comment lines.
  5. First statement is a rule (.+ {200};): it compiles as "U+FEFF, then .+". Ordinary text has no U+FEFF, so the rule does not match it and ICU falls back to breaking after each character with status 0. Directly in ICU4J: .+ {200}; on ab cd gives its first boundary at 5 with status 200; with U+FEFF in front, at 1 with status 0. The tokenizer skips all of it. No error.
  6. First statement is a directive or a variable definition: U+FEFF !!forward; is ICU error 66052 (U_BRK_RULE_SYNTAX), reported at the start of the directive (Lucene's two files: line 20 or 44, column 1, after their comment lines); U+FEFF $Var = ...; reads as a use of an undefined variable, error 66058, reported just past the variable name (the independent reproduction's $Ws file: line 1, column 5; with $Whitespace, column 13). Neither message names the invisible character at line 1, column 1.

The numbers

rule filewithout BOMwith BOM
.+ {200};, text ab cd[ab cd]no tokens, no error
[\p{Letter}]+ {200}; then [\p{Number}]+ {100};, text ab 12 cd[ab] [12] [cd][12]
Lucene's KeywordTokenizer.rbbi for Cyrillic and Thai, upstream test text6 tokens4 tokens: [Some] [English] [More] [English]
Lucene's Latin-break-only-on-whitespace.rbbi (!!forward; on line 20)2 tokensError 66052 at line 20 column 1
Lucene's Latin-dont-break-on-hyphens.rbbi (!!chain; on line 44)2 tokensError 66052 at line 44 column 1
The independent reproduction's file whose first line is $Ws = [\p{Whitespace}];[ab] [cd]Error 66058 at line 1 column 5 (just past the variable name)

The default configuration (no rulefiles) loads precompiled Default.brk and MyanmarSyllable.brk (DefaultICUTokenizerConfig.java:81-84, :147) and never runs parseRules; none of Lucene's five checked-in .rbbi files starts with a BOM (the build-time sources Default.rbbi and MyanmarSyllable.rbbi, and the three test files).

Exposure and likelihood274 words
  • Who is exposed. Applications that opt in to custom per-script rules: ICUTokenizerFactory with rulefiles, directly, through the icu SPI name, or through a search server schema built on it (the factory javadoc shows such a schema).
  • What it takes. One custom rule file saved as UTF-8 with a BOM. Editors such as Windows Notepad often write one (the SOLR-1865 change note says so), and ICU's genbrk(1) man page recommends one.
  • Silent case. The first statement is a rule, the shape of Lucene's KeywordTokenizer.rbbi: no error; text that only the first rule covers gets no tokens. With a one-rule file that is all text of the script, which is then not indexed and cannot be found; an index built that way needs reindexing after the fix.
  • Loud case. The first statement is a !! directive or a variable definition, the shape of two of Lucene's three test rule files: the analyzer fails to build with an ICU syntax error whose position points at that statement (a directive's line, or just past a variable's name), not at the invisible BOM at line 1, column 1. ICU's own rule guide calls stray literals "surprisingly hard to recognize and debug".
  • Scale. Deterministic: the rules are compiled once, when the analyzer is built, whatever the index size. Which tokens are lost depends on the text and on which rules cover it.
  • How likely. Rare (registry: rare, priority low): opt-in, needs the BOM, a file that starts with a directive or a variable fails loudly (two of Lucene's three test files have that shape), and no report was found in the searched public sources since 4.1.0 (January 2013).

Minimal repro

The test shows that ICUTokenizerFactory keeps a UTF-8 byte order mark at the start of a rulefiles rule file as rule text: the file <BOM>.+ {200}; loads without error, then the Latin text is dropped.

TestICUTokenizerFactoryRuleFileBom.java (JUnit test)48 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.analysis.icu.segmentation;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.StringMockResourceLoader;

public class TestICUTokenizerFactoryRuleFileBom extends LuceneTestCase {

  public void testRuleFileWithUtf8Bom() throws Exception {
    ICUTokenizerFactory factory =
        new ICUTokenizerFactory(new HashMap<>(Map.of("rulefiles", "Latn:latin.rbbi")));
    factory.inform(new StringMockResourceLoader("\uFEFF.+ {200};\n"));
    List<String> tokens = new ArrayList<>();
    try (Tokenizer tokenizer = factory.create()) {
      CharTermAttribute term = tokenizer.addAttribute(CharTermAttribute.class);
      tokenizer.setReader(new StringReader("ab cd"));
      tokenizer.reset();
      while (tokenizer.incrementToken()) {
        tokens.add(term.toString());
      }
      tokenizer.end();
    }
    // same tokens as for the file without the BOM; actual: none (the BOM became rule text)
    assertEquals(List.of("ab cd"), tokens);
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-719/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble   (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:$LUCENE/lucene/analysis/icu/build/libs/lucene-analysis-icu-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar   # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar   # JUnit Platform console launcher (Maven Central)

javac -d classes -cp "$CP" $(find src -name '*.java')

# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.analysis.icu.segmentation.TestICUTokenizerFactoryRuleFileBom
# the module(s) analysis-icu also need their third-party runtime jars
Expected vs actual
Expected [ab cd] for ab cd (as without the BOM); actual [] (expected:<[ab cd]> but was:<[]>), with -ea and -da; passes when parseRules skips a leading U+FEFF.

All files of this repro: repro/LFV-719/

More files, environment, full output35 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: expected:<[ab cd]> but was:<[]>
       org.junit.Assert.fail(Assert.java:89)
       org.junit.Assert.failNotEquals(Assert.java:835)
       org.junit.Assert.assertEquals(Assert.java:120)
       org.junit.Assert.assertEquals(Assert.java:146)
       org.apache.lucene.analysis.icu.segmentation.TestICUTokenizerFactoryRuleFileBom.testRuleFileWithUtf8Bom(TestICUTokenizerFactoryRuleFileBom.java:46)

Why it looks unintended

  • ICU's own rule-file compiler, genbrk, strips a leading BOM; its man page recommends saving rule files "in UTF-8 with the BOM".
  • Lucene's word-list and hunspell readers skip a leading BOM; this reader never did, and no document or test covers its behaviour.
  • Lucene's own KeywordTokenizer.rbbi with a BOM loses the Cyrillic and Thai tokens its upstream test asserts.
The full argument252 words
  • The factory owns the file reading. It turns bytes into a String and already pre-processes the file (blanking # lines) before handing a String to ICU (ICUTokenizerFactory.java:137-151). ICU maintainers place the encoding signature in the file reader, not the string API (ICU-21907, open: genbrk "detects and honors a BOM"; "the rbbi API for building from rules takes a UnicodeString. The code page stuff is in the genbrk tool").
  • ICU's reader for this file format strips the BOM. genbrk.cpp calls ucnv_detectUnicodeSignature and skips the signature (ruleSourceC += signatureLength); its man page (CAVEATS) says "it is recommended that you write this file in UTF-8 with the BOM". (The man page's sentence that a BOM-less file is read in the operating system's codepage is stale: the code assumes UTF-8.)
  • Lucene's other resource readers skip it. WordlistLoader.getLines ("skip initial bom marker", WordlistLoader.java:302-304, SOLR-1865, 2010) and the hunspell Dictionary (Dictionary.java:246, :351-352; LUCENE-5505, listed under "Bug fixes" in 4.8.0). parseRules (2012, more than two years later) never adopted it.
  • Nothing documents or tests the current behaviour. The javadoc says only that rule files "are compiled by the ICU RuleBasedBreakIterator" and links ICU's syntax page (ICUTokenizerFactory.java:49-52), which says nothing about encodings. SOLR-4123's 21 JIRA comments never discuss encoding, no later commit on the file mentions a BOM, and all five checked-in .rbbi files are BOM-free.
  • Classification is open. No Lucene document promises BOM handling for rule files, so upstream may call the fix an improvement: SOLR-1865 was typed Improvement.
The devil's-advocate check268 words

An independent reviewer from another model family (OpenAI GPT-5.6), refute lens, executed, without network access: "not real, high", although it reproduced the silent no-token outcome. (a) The javadoc delegates the format to ICU, and RuleBasedBreakIterator(String) treats U+FEFF as a literal by design; Java's UTF-8 decoder keeps it on purpose. (b) Lucene's build-time RBBIRuleCompiler.getRules keeps a BOM the same way. (c) BOM skipping in WordlistLoader and hunspell is component-specific, and Lucene bans BOMs in its own source tree (SOLR-9295, #12937). (d) In 2010 a committer answered "No." to "should we consider that a bug?" about BOMs in Solr's resource files.

Why it fails: (a) the factory reads a file, and ICU's own file reader for this format strips the BOM; the reviewer did not have ICU's genbrk source or man page, and the same reviewer family, given the genbrk man page's CAVEATS section, rated the case real (high), intended-behaviour argument moderate. (b) RBBIRuleCompiler compiles only Lucene's own BOM-free files at build time. (c) The source-tree ban is repository hygiene, not a rule for users' files. (d) That remark predates rule files by more than two years, and the same reply went on: "we can ignore them for the first line of the file to be more user-friendly. I'll open an issue" (SOLR-1865); in 2014 the same slip in hunspell went under "Bug fixes". Two points stand: rule files are trusted configuration, and ICU-2003 (2002, Won't Fix) declined to make ICU's own rule parsers ignore such characters; both fit fixing it in Lucene's file reader (ICU-21907) but keep the classification open. What survives is the bug-or-improvement classification, not the defect.

Suggested fix

Recommended Skip a BOM at the start of the first line (recommended, validated)

In ICUTokenizerFactory.parseRules, drop a U+FEFF at the start of the first line before the # comment check, the idiom WordlistLoader.getLines already uses (+6 lines). The 3 new tests pass, and so do the ICU module's 161 tests and its static checks.

    String line = null;
    boolean firstLine = true;
    while ((line = reader.readLine()) != null) {
      // skip initial bom marker
      if (firstLine && line.length() > 0 && line.charAt(0) == '\uFEFF') {
        line = line.substring(1);
      }
      firstLine = false;
      if (!line.startsWith("#")) {
        rules.append(line);
      }
All options and trade-offs4 options ยท 483 words

One fix was applied in a scratch worktree of a9779ef3c193 and run with gradle, assertions on and off; the worktree was removed afterwards. The other options were not run.

Skip a BOM at the start of the first line (recommended, validated)

Before the # test, remove a U+FEFF at position 0 of the first line. The first comment line is then blanked as intended, and every other line, and so every line number in ICU's error messages, stays as in the file. Same idiom as WordlistLoader.getLines:302-304, with braces; the hunt's sketch is the same change on one line.

    String line = null;
    boolean firstLine = true;
    while ((line = reader.readLine()) != null) {
      // skip initial bom marker
      if (firstLine && line.length() > 0 && line.charAt(0) == '\uFEFF') {
        line = line.substring(1);
      }
      firstLine = false;
      if (!line.startsWith("#")) {
        rules.append(line);
      }

Trade-offs

Only a U+FEFF at the very start of the file is removed; one later in the file, a legitimate literal, is untouched. A file that deliberately began its first rule with a literal U+FEFF would change meaning; ICU's own genbrk strips that character too. Scope: a UTF-8 BOM only (the factory decodes strictly as UTF-8, so UTF-16 files stay unsupported); a file that holds only a BOM was not tested. No CHANGES entry is drafted.

What the checks validated

  • The 3 new tests and the 5 upstream TestICUTokenizerFactory tests pass, assertions on and off (stock: the 3 new tests fail).
  • All of :lucene:analysis:icu:test, assertions on: 17 classes, 161 tests, 10 skipped, 0 failures.
  • :lucene:analysis:icu:check -x test passes with the new test in place (google-java-format, ecjLint and forbiddenApis on main, test and tools sources, checkLicenses).
  • The public-API probe with the patched classes: every BOM file gives exactly the tokens of its BOM-less copy, including the two-rule file and Latin-dont-break-on-hyphens.rbbi.
  • Earlier, in the repro harness with the hunt's one-line version: the finder's repro 3/3, the reproducer's 10/10, the devil's advocate's 6/6 and the version-independent check on main pass.

Strip a leading U+FEFF from the assembled rule text (not run)

if (rules.length() > 0 && rules.charAt(0) == '\uFEFF') rules.deleteCharAt(0); before compiling.

Trade-offs

One line. Lucene would no longer blank the first line, but ICU reads # ... as a comment anyway, so the result should be the same; not executed.

Reject a BOM with a clear message (not run)

Throw an IllegalArgumentException naming the file and the byte order mark.

Trade-offs

Nothing is lost silently and the message is precise, but it refuses a file that ICU's own tool accepts and recommends.

One BOM-aware reader for every resource reader that bypasses WordlistLoader.getLines (not run)

Use one small helper here and in the snowball word-set loader, the synonym file parsers and the Kuromoji and Nori user dictionaries, which keep a BOM the same way.

Trade-offs

Fixes the whole class at once but touches several modules; worth raising if upstream wants one issue.

Downstream note

Two downstream search servers' ICU plugins have their own copy of this rule-file reader with the same pattern, so a Lucene fix would not reach them.

How it was found

A bounded enumeration ran the same rule through the tokenizer in 9 file layouts (line ends, comments, blank lines, a leading BOM) and 8 spellings of the rulefiles argument, each compared with the plain file, plus a default-configuration control: 18 checks, 2 mismatches, both with a BOM. The idea came from a sibling BOM bug in a word-list loader.

The method in full158 words

Method (semi-formal): bounded enumeration in a hunt unit on analysis-factory resource file formats, on the stock main jar.

  • The harness tried 8 spellings of the rulefiles argument and 9 layouts of the same rule file: with and without a final newline, LF and CRLF line ends, comment lines, indented and trailing comments, blank lines, and a leading BOM. Each was checked through the tokenizer against the tokens of the plain BOM-less file, plus one default-configuration control: 18 checks, 2 mismatches, both BOM files (.+ {200};, alone and after a # line).
  • The hypothesis came from extending the unit's earlier finding, a BOM kept by the snowball word-set loader (LFV-718), to the other resource readers that do not go through WordlistLoader.getLines.
  • The finder reduced it to a public-API JUnit test; no model checker, solver or proof was involved. Writing this doc, a probe added the two-rule case, which shows that only the first rule is broken.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues, PRs and all 410 open PRs, Lucene, Solr and ICU JIRA, the Lucene and Solr mailing lists or the web (searched 2026-10-27). SOLR-1865 and LUCENE-5505 fixed the same slip in other readers.

Searches5 searches

No report of this defect was found. The nearest items fixed the same slip at other fix sites or discuss BOM handling in ICU itself.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene), downstream repos, Lucene mailing lists, LUCENE JIRA archive2026-10-27a scripted prior-report search, two runs: 44 GitHub, 6 commit, 6 downstream, 36 list (dev@, java-user@, issues@, commits@) and 9 JIRA queries, plus 8 web queries201 and 648 distinct hits, none about this bug
GitHub (apache/lucene, apache/solr, two downstream search servers)2026-10-2727 manual issue and PR searchesno BOM or rule-file encoding report
Open apache/lucene PRs2026-10-27changed files of all 410 open PRsonly #16911 (branch_10x decommission) touches the file
SOLR, LUCENE and ICU JIRA; Solr and Lucene lists2026-10-278 SOLR/LUCENE and 5 ICU JQL queries, 25 list queries (the web searches are the 8 queries counted above)SOLR-4123 (origin, no encoding discussion); ICU-21907 (BOM handling belongs to the file reader); ICU-2003 (2002, Won't Fix); none reports this bug
Related, at other fix sites2026-10-27readSOLR-1865 (word-list loader, Improvement), #6568 / LUCENE-5505 (hunspell, Bug); #12937 and SOLR-9295 are source-tree hygiene

History

Introduced with the rulefiles feature (SOLR-4123, 2012-12-03), first released in 4.1.0, unchanged since and not fixed on main. Every executed release from 4.1.0 to 10.5.2 is affected; 4.0.0 has no such feature.

Versions tested6 rows

Introduced by SOLR-4123 "ICUTokenizerFactory - per-script RBBI customization" (JIRA, Improvement; Subversion era, no PR): trunk 6024e1465e0 and the branch_4x merge 9a23a0799e5, both 2012-12-03, committed by Steve Rowe; the 4.1.0 change note credits Shawn Heisey, Robert Muir and Steve Rowe. It added rulefiles and parseRules with no BOM handling. Later commits only changed the charset constant (LUCENE-5560, 2014) and the formatting (LUCENE-9570, 2021).

Not a regression and not tied to an ICU4J version (the bundled 49.1, 53.1, 54.1, 56.1, 62.1, 68.2, 74.2, 77.1 and 78.3 behave the same). In the history clone's release tags the whitespace-normalized parseRules body has three forms, none of which changes behaviour: 4.1.0 to 4.7.2, 4.8.0 to 8.8.1 (charset constant) and 9.0.0 to 10.5.2 (reformat). The clone has no tags for 8.8.2 to 8.11.4; 8.11.4 was executed, and the releases not executed are inferred to be affected from the unchanged code. Upstream main c5ab3e57b9b (2026-10-28) has the same ICU module as a9779ef3c193, with no BOM handling.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectednew test 3/3 fail (gradle, -ea and -da); finder's 2/3, reproducer's 4/10 fail; ICU4J 78.3
10.0.0, 10.5.2 affectedfinder's JUnit 2/3 fail; version-independent check fails with both outcomes; ICU4J 74.2, 77.1
9.0.0, 9.12.3 affectedboth outcomes; ICU4J 68.2, 74.2
4.1.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4 affectedboth outcomes (version-independent public-API check); ICU4J 49.1 to 62.1
4.0.0 no featurethe factory ignores rulefiles and never opens the file
main with the fix fixevery BOM file tokenizes like its BOM-less copy

Independent checks

A finder, an independent reproducer, an intended-behaviour devil's advocate, cross-model reviews (OpenAI GPT-5.6) and an adjudication: confirmed, medium confidence on the bug-or-improvement framing, not on the defect. One review said not real; it lacked ICU's rule-file documentation.

Checks and verdicts213 words

Every check except the refute review returned real; all of them reproduced the behaviour.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidatePublic-API JUnit fails 2 of 3 on main (-ea, -da), 10.5.2 and 9.12.3; passes with its fix sketch, with the 5 upstream factory tests.
refute review (OpenAI GPT-5.6) not real (high)Reproduced the silent no-token outcome but argued there is no BOM promise; ran without network access and without ICU's genbrk documentation.
intended review (OpenAI GPT-5.6) real (high)Given ICU's genbrk CAVEATS: intended-behaviour argument moderate; root cause and fix agree.
independent reproducer realOwn raw-byte JUnit fails 4 of 10 on main, 10.5.2 and 9.12.3; ICU4J mechanism control; 10 of 10 with the fix.
root-cause review (OpenAI GPT-5.6) real (high)Instrumentation shows U+FEFF as the first code point handed to ICU; the fix fixes it.
devil's advocate real (medium)BOM copies of the three upstream fixtures fail 3 of 6; intended-behaviour argument weak-to-moderate.
adjudication confirmed (medium)Re-ran the reproducer's and the finder's tests with assertions on and off, and the fix.
registry re-run, audit and this doc keepRe-runs fail on main and pass with the fix; this doc's gradle validation and the two-rule probe.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,459 words

Finding record

LFV-719, snapshot of 2026-11-06

Registry title
ICUTokenizerFactory keeps the UTF-8 byte order mark at the start of a custom per-script rule file (rulefiles) as rule text, so the file either fails with a misleading ICU syntax error or loads without error and the tokenizer then emits no tokens for that script (every release since 4.1.0)
Area
analysis / ICU (ICUTokenizerFactory per-script rule files: rulefiles, parseRules)
Priority
Low, rank 503: This is the same kept-BOM slip in ICUTokenizerFactory's opt-in custom rule files, where most such files fail loudly and only a file that starts with a rule silently loses tokens.
Impact class
silent wrong results
Impact note
Only applications that opt in to custom per-script ICU rule files (the rulefiles argument of ICUTokenizerFactory, analysis-icu module) and save such a file as UTF-8 with a byte order mark are hit, at analyzer setup: a file whose first statement is a rule (for example '.+ {200};', alone or after '#' comment lines, the shape of Lucene's own test file KeywordTokenizer.rbbi) compiles without error and every token of that script is then silently dropped, while a file that starts with a '!!' directive or a variable definition is rejected with an IllegalArgumentException whose position does not point at the BOM; the default configuration (no rulefiles) is not affected, no scale is needed, and it is the same with -ea and -da in every release from 4.1.0 to 10.5.2 [exec] and, by reading, 10.6.0.
Summary
You configure ICUTokenizerFactory with rulefiles="Latn:latin.rbbi" and a rule file saved as UTF-8 with a byte order mark (BOM), as editors such as Windows Notepad often write and as ICU's own rule-file compiler genbrk accepts (it strips the BOM, and its man page recommends one), and expect the same tokens as with the same file without the BOM. Instead ICUTokenizerFactory.parseRules decodes the BOM to U+FEFF and passes it to ICU's RuleBasedBreakIterator(String) as rule text: a file that starts with the rule '.+ {200};' (alone or after '#' comment lines) compiles with U+FEFF as a literal prefix of that rule, so 'ab cd' gives no tokens at all instead of ['ab cd'], and a file that starts with a comment and '!!forward;' (the shape of Lucene's Latin-break-only-on-whitespace.rbbi) fails with 'IllegalArgumentException: Error 66052 at line 2 column 1'. Nothing on disk is corrupted, but in the silent case all text of the tailored script disappears from the analyzed output with no error.
Introduced
6024e1465e02 (svn trunk r1416617, 2012-12-03, by sarowe), SOLR-4123 'Add per-script customizability to ICUTokenizerFactory via rule files in the ICU RuleBasedBreakIterator format' (Subversion and JIRA era, no PR; none of the 21 JIRA comments mentions a BOM or the file encoding); merged to branch_4x the same day as 9a23a0799e5e (r1416629), the commit that shipped in 4.1.0. It added the rulefiles argument and parseRules, which reads the file through a UTF-8 decoding reader, drops '#' lines and never removes a leading U+FEFF [read: git log -S parseRules, git blame -w -M -C -C, git log -L in the full-history clone] [exec: 4.0.0 ignores rulefiles, 4.1.0 fails]. Later commits on parseRules changed no behaviour (2f8192f3c5a, LUCENE-5560, 2014, charset constant; 7f8b7ffbcad, LUCENE-9570, 2021, reformatting), and git log --all -G 'FEFF|bom' on the file finds no commit. Not a regression and not tied to an ICU4J version (2012-12-03)
First affected version
4.1.0 [exec] (the first release with rulefiles; 4.0.0 has no inform(ResourceLoader) in this factory, ignores the argument and never opens the file [exec])
Affected versions
Every release from 4.1.0 to 10.5.2, 10.6.0 (by reading) and main a9779ef3c193; never fixed. [exec] the follow-up's portable public-API check fails with both outcomes on 4.1.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (BOM-less controls give the custom-rule tokens everywhere) and passes on main with the fix shadowed; the finder's JUnit fails 2 of 3 on 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da on main); the independent reproduction's JUnit fails 4 of 10 on main (-ea and -da), 10.5.2 (-ea and -da) and 9.12.3 (-ea); a separate test fails on main (-ea and -da), 10.5.2 and 9.12.3; the intended-behaviour check's probe fails 3 of 6 on main (-ea and -da) and 10.5.2. The bundled ICU4J versions from 49.1 to 78.3 all behave the same. [read] the normalized parseRules body is identical in every release tag of the history clone from 4.1.0 to 8.8.1 and from 9.0.0 to 10.5.2 (8.9-8.11 are untagged there; 8.11.4 was executed). Not fixed upstream: ICUTokenizerFactory.java has no BOM handling on upstream main a95c754ee003, branch_10x 1309534296e or branch_10_6 2763db11b0a, and lucene/analysis/icu is unchanged between a9779ef3c193 and a95c754ee003 (local fetch of 2026-10-26, read 2026-10-27). Not affected: 4.0.0 and older (no rulefiles feature). All runs on Linux aarch64 with JDK 25
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it (the adjudication ruled it real), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal
Upstream and prior reports
none found (2026-10-27, completed, read-only; the prior-report record verdict new): two scripted prior-report searches (44 GitHub issue and PR, 6 commit, 6 downstream, 36 mailing-list over dev@, java-user@, issues@ and commits@, and 9 JIRA-archive queries, plus 8 ingested web queries; 201 and 648 distinct hits, 0 high), 27 manual GitHub searches on apache/lucene, apache/solr, Elasticsearch and OpenSearch, the changed files of all 410 open apache/lucene PRs (only #16911, the branch_10x decommission, touches ICUTokenizerFactory.java), 8 SOLR and LUCENE and 5 ICU JIRA queries, 25 list queries (solr-user@, users@solr, dev@solr, dev@lucene, java-user@), 10 web queries and the git history; the independent reproduction's and the intended-behaviour check's own searches agree. No report of a BOM in an ICU rule file, of the misleading 'Error 66052', or of a script losing its tokens because of it. Related, not this bug: SOLR-1865 (BOM skip in the line loader that became WordlistLoader.getLines, typed Improvement) and #6568 / LUCENE-5505 (hunspell BOM, typed Bug, listed under Bug fixes), precedents at other fix sites; ICU-21907 (open), where ICU maintainers say genbrk 'detects and honors a BOM' and that the RuleBasedBreakIterator API takes a string, placing BOM handling in the file reader; ICU-2003 (Won't Fix, duplicate of ICU-1997, 2002), an ICU-side request that rule parsers ignore format characters such as a leftover BOM; SOLR-4123 (origin of the code, no encoding discussion); #7747 / LUCENE-6689 (open, names rulefiles, unrelated WordDelimiterFilter problem); #12937 and SOLR-9295 (BOM hygiene of Lucene's own source tree, not user files). Two downstream projects carry their own copies of parseRules with the same pattern and no BOM report. Not fixed upstream (main a95c754ee003, branch_10x, branch_10_6, read).
Lucene commit tested
main a9779ef3c193 (2026-10-26) through gradle in a scratch worktree: a new 3-test class with the 5 upstream TestICUTokenizerFactory tests, assertions on and off (stock: 3 of 8 fail; with the recommended fix: 0), the fix with all of :lucene:analysis:icu:test (17 classes, 161 tests, 10 skipped, 0 failures) and :lucene:analysis:icu:check -x test; the worktree was removed afterwards. A public-API probe on the stock main jars and with the patched classes. The hunt's repros on main (-ea, -da), 9.12.3, 10.0.0 and 10.5.2, and its version-independent check on 4.0.0 (no such feature), 4.1.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main; OpenJDK 25.0.3, Linux aarch64.

Lucene and upstream sources

Finding record
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizerFactory.java:49-52 (rule files 'compiled by the ICU RuleBasedBreakIterator', link to the syntax page), :54-66 (rulefiles format), :86-95 (rulefiles parsing), :108-135 (inform), :137-151 (parseRules: UTF-8 decoding reader, '#' lines dropped, no BOM skip)
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:208-213 (segments with rule status 0 are skipped); DefaultICUTokenizerConfig.java:82-84, :147 (the default configuration loads precompiled .brk files)
  • lucene/core/src/java/org/apache/lucene/analysis/WordlistLoader.java:302-304 ('skip initial bom marker' in getLines)
  • SECURITY.md ('Other out-of-scope reports': configuration values are trusted as given; 'In-scope reports')
  • a Lucene clone origin/main a95c754ee00, origin/branch_10x 1309534296e, origin/branch_10_6 2763db11b0a: ICUTokenizerFactory.java has no FEFF or BOM handling; git diff a9779ef3c193 origin/main -- lucene/analysis/icu is empty (read 2026-10-27)
The finding in 30 seconds
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizerFactory.java:86-95, :108-135, :137-151 (main a9779ef3c193)
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:209-213 (status-0 segments skipped)
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/DefaultICUTokenizerConfig.java:81-84, :147 (default path: precompiled .brk)
  • javap -constants on ICU4J 78.3 com.ibm.icu.text.RBBIRuleBuilder: U_BRK_RULE_SYNTAX = 66052, U_BRK_UNDEFINED_VARIABLE = 66058 [exec]
Who is affected
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizerFactory.java:49-66 (javadoc, schema example)
Why it looks unintended
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizerFactory.java:49-52, :137-151
  • lucene/core/src/java/org/apache/lucene/analysis/WordlistLoader.java:302-304
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/hunspell/Dictionary.java:246, :351-352
  • lucene/CHANGES.txt:12543-12555 (4.8.0 Bug fixes, LUCENE-5505)
  • lucene/analysis/icu/src/tools/java/org/apache/lucene/analysis/icu/RBBIRuleCompiler.java:46-60
Suggested fix
  • lucene/core/src/java/org/apache/lucene/analysis/WordlistLoader.java:302-304
History
  • lucene/CHANGES.txt:14654-14656 (4.1.0, SOLR-4123 credits)
  • a Lucene clone: git diff a9779ef3c193 origin/main (c5ab3e57b9b, 2026-10-28) -- lucene/analysis/icu is empty; no FEFF or BOM in ICUTokenizerFactory.java there [read]

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.