All findings · Lucene finding · LFV-971 · lucene90-compound-entries-javadoc-missing-footer

The .cfe grammar in the Lucene90CompoundFormat javadoc leaves out the footer and misstates what FileName holds

The javadoc of the default compound file format describes the .cfe table of contents without the 16-byte checksum footer the writer appends, and says each entry holds the file's name, while the writer stores it without the segment name (.fnm for _0.fnm). Documentation only: Lucene reads its own files correctly.

Summary

Priority
Low, rank 1285The .cfe grammar leaves out the footer, which the package-level footer rule partly covers, and misstates the stored names; it is documentation only.
Impact
minor
Since
5.0.0 for the footer (LUCENE-2446's trunk text, released through LUCENE-5944 in 2014); 4.0.0 for the FileName wordingintroduced 2014-04-01 · over 12 years
Affected
5.0.0-10.6.0 and main
Status
documented
Found by
semi-formal: the javadoc grammar checked against 87 real .cfe files

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

The class javadoc that documents the default compound file's table of contents leaves out a part the writer writes, and misdescribes another.

Background: compound files

  1. To save file handles, Lucene by default packs a newly flushed segment's files (and a merged segment's, if it is small enough) into one compound file: _0.cfs holds the bytes of _0.fnm, _0.fdt and the others one after another (each start aligned to 64 bytes), and _0.cfe is its table of contents, one entry per file (name, start offset, length).
  2. Like every index file, .cfe starts with an IndexHeader (magic, codec name, version, the 16-byte segment id and a suffix) and ends with a 16-byte codec footer (magic, algorithm id, CRC32 checksum).

The finding, step by step

  1. You index one empty document with the default codec and compound files on (the default for new segments). Lucene packs the segment's 4 files into _0.cfs and writes their 150-byte table of contents, _0.cfe.
  2. You decode _0.cfe as the Lucene90CompoundFormat javadoc says: Header (49 bytes), FileCount (4), then 4 entries of FileName, DataOffset, DataLength (21 bytes each). You expect the file to end at byte 134, and the names to be _0.fnm, _0.fdx, _0.fdt, _0.fdm.
  3. You get 16 more bytes after the last entry: the codec footer (c0 28 93 e8, algorithm id, CRC32 checksum). The .cfs line right above lists its Footer; the .cfe line does not.
  4. And the names read .fnm, .fdx, .fdt, .fdm: the writer strips the segment name _0, and Lucene's reader adds it back. Lucene reads its own files fine; only a tool written from the javadoc goes wrong, on every .cfe file since 5.0.0.

Who is affected

People who read, inspect or re-implement the .cfe format from this javadoc: format inspectors, ports, recovery or extraction tools. The default codec's overview links this page for the details. Every .cfe file since 5.0.0 is affected, with no setting involved. Lucene's writer and reader agree, so indexing, merging, CheckIndex and search are unaffected.

Mechanism, terms and numbers788 words

A compound segment is two files: .cfs holds the bytes of all the segment's files and .cfe is its table of contents. The class javadoc of Lucene90CompoundFormat, linked from the default codec's file-format overview as "Compound File", gives Compound (.cfs) --> Header, FileData^FileCount, Footer (Lucene90CompoundFormat.java:49 on main 645d4fe14a20) and Compound Entry Table (.cfe) --> Header, FileCount, <FileName, DataOffset, DataLength>^FileCount (:50-51), with no Footer on the .cfe line, and says each entry holds "a String with that file's name" (:65-66). The writer appends a 16-byte codec footer to .cfe (:107) and stores each name with the segment name stripped (:153), for example .fnm for _0.fnm. A decoder that follows the text stops 16 bytes before the end of every .cfe file, and a lookup by the names it read, taken as file names, fails (the segment name is missing). Lucene's reader checks the footer (Lucene90CompoundReader.java:133) and adds the segment name back (:222), so only the javadoc is wrong: the footer part in every release from 5.0.0 to 10.6.0 and on main, the FileName part from 4.0.0 on. The unused Checksum on the type line (:54) belongs to the linked CodecFooter grammar; removing it is cleanup.

The documented grammar (main 645d4fe14a20, Lucene90CompoundFormat.java:49-67)

Compound (.cfs) --> Header, FileData^FileCount, Footer
Compound Entry Table (.cfe) --> Header, FileCount, <FileName, DataOffset, DataLength>^FileCount

  Header --> IndexHeader                     (CodecUtil#writeIndexHeader)
  FileCount --> VInt
  DataOffset,DataLength,Checksum --> UInt64
  FileName --> String
  FileData --> raw file data
  Footer --> CodecFooter                     (CodecUtil#writeFooter)

Notes: ... Each directory entry contains a long pointer to the start of this file's data section,
the files length, and a String with that file's name. ...

The .cfe production has no Footer, although Footer is defined (:57) and used on the .cfs line (:49). Checksum (:54) is used by neither production: it is the last element of the linked CodecFooter grammar (CodecUtil.java:396).

What the writer writes (write, :95-109; writeCompoundFile, :113-157)

  • CodecUtil.writeIndexHeader(entries, ENTRY_CODEC, ...) (:102), entries.writeVInt(numFiles) (:117).
  • Per file, in ascending size order: entries.writeString(IndexFileNames.stripSegmentName(file)), writeLong(startOffset), writeLong(length) (:153-155). stripSegmentName returns "the filename with the segment name removed" (IndexFileNames.java:133-144); it cuts at the second _ or else the first . (:119-127), so _0.fnm is stored as .fnm, while a per-field file keeps the _ of its suffix (the history probe saw _Lucene50_0.doc stored on 5.0.0-7.7.3).
  • Then CodecUtil.writeFooter(entries) (:107), right after CodecUtil.writeFooter(data) for the .cfs (:106).
  • The reader mirrors it: readMapping reads the string as an id (Lucene90CompoundReader.java:144), CodecUtil.checkFooter verifies the footer (:133), lookups strip the segment name from the requested name (:171, :235), and listAll adds it back (:222).

The bytes of the repro's file (_0.cfe, 150 bytes, main)

bytes 0-48     IndexHeader (49 bytes: magic, "Lucene90CompoundEntries", version, segment id, empty suffix)
byte  49       FileCount = 4 (VInt)
bytes 50-70    FileName ".fnm"  DataOffset 64   DataLength 61    (21 bytes: 1 + 4 + 8 + 8)
bytes 71-91    FileName ".fdx"  DataOffset 128  DataLength 64
bytes 92-112   FileName ".fdt"  DataOffset 192  DataLength 78
bytes 113-133  FileName ".fdm"  DataOffset 320  DataLength 157
               <- the documented grammar ends here, at byte 134 of 150
bytes 134-149  Footer: c0 28 93 e8 | 00 00 00 00 | 8-byte CRC32 checksum   (not in the .cfe grammar)

The segment id and the checksum differ per run; this one-document index's layout does not. CodecUtil.checkFooter accepts the 16 left-over bytes. The compound directory lists the sub-files as _0.fnm, _0.fdx, _0.fdt, _0.fdm. In the hunt's sweep the same holds for all 87 .cfe files of 91 generated indexes (16 bytes left over in each) and all 1720 entries (stripped names).

How it went stale

  • Footer. LUCENE-2446 added writeFooter to both compound files in one change. On trunk (c189d0fb74d, 2014-04-01) the javadoc got , Footer on the .cfs line only, plus Checksum on the type line; the branch_4x backport (819eda82e33, 2014-04-02) put , Footer on the .cfe line instead, so 4.8.0-4.10.4 documented the .cfe footer. The LUCENE-5944 bulk backport (55594bad9b0, 2014-09-18) replaced the branch_5x text with the trunk text, so from 5.0.0 the .cfe line has no Footer. LUCENE-5969 and LUCENE-9705 copied the text into Lucene50CompoundFormat and Lucene90CompoundFormat.
  • FileName. Up to 3.0.x the writer stored the full name and "with that file's name" was right. LUCENE-2455 (d6dd2e234e1, 2010, 3.1) started storing the stripped name, so that entries stay valid when a segment is renamed, and the sentence was not updated; 25a59d8902e (2012-06-11) moved it into the public format docs (4.0.0).
Exposure and likelihood223 words
  • Who is hit. Anyone who decodes a .cfe file without Lucene, or checks one by hand, following the class javadoc. The default codec's overview (lucene104/package-info.java) says it "defines the index file formats used in this version of Lucene" (:50), links this class as "Compound File" (:237) and says "Details on each of these are provided in their linked pages" (:188). Two outside .cfe parsers exist (a 2021 draft format description and a 2018 script); both worked around the text without reporting it.
  • Default vs opt-in. The default codec; compound files are the default for new segments (IndexWriterConfig.java:96-99, DEFAULT_USE_COMPOUND_FILE_SYSTEM = true) and for small segments (package-info.java:195). Every .cfe file Lucene90CompoundFormat writes shows both parts; no setting changes its layout (a segment that is not compound has no .cfe at all).
  • Scale. No runtime effect. The symptom is confined to code written from the text: a decoder stops 16 bytes early and looks for .fnm where the compound directory has _0.fnm; a tool that wrote .cfe files by the text would produce files Lucene's reader rejects at checkFooter (read, not executed). A careful reader who cross-checks the overview's footer note and the reader code can work it out, which keeps the impact minor.
  • Rating. Impact class minor, likelihood plausible, priority low.

Minimal repro

One JUnit class, public API: index one empty document with the default codec and compound files on, then read _0.cfe with only the documented grammar. One method per wrong statement: the bytes left after the last entry, and the stored names. Deterministic, single thread, about 2.6 s; the same with assertions off.

TestLucene90CompoundEntriesJavadoc.java (JUnit, one class, one method per wrong statement)80 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.codecs.lucene90;

import java.util.Arrays;
import java.util.TreeSet;
import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.codecs.CompoundDirectory;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.SegmentInfo;
import org.apache.lucene.index.SegmentInfos;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

public class TestLucene90CompoundEntriesJavadoc extends LuceneTestCase {

  private static SegmentInfo writeCompoundSegment(Directory dir) throws Exception {
    IndexWriterConfig iwc =
        new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()).setUseCompoundFile(true);
    try (IndexWriter w = new IndexWriter(dir, iwc)) {
      w.addDocument(new Document());
    }
    return SegmentInfos.readLatestCommit(dir).info(0).info;
  }

  public void testEntryTableEndsAfterLastEntry() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      SegmentInfo si = writeCompoundSegment(dir);
      try (IndexInput in = dir.openInput(si.name + ".cfe", IOContext.READONCE)) {
        in.seek(CodecUtil.indexHeaderLength("Lucene90CompoundEntries", ""));
        for (int i = in.readVInt(); i > 0; i--) {
          in.readString();
          in.readLong();
          in.readLong();
        }
        // javadoc: .cfe ends after the last <FileName, DataOffset, DataLength>; written: a Footer
        assertEquals("bytes after the documented grammar", 0, in.length() - in.getFilePointer());
      }
    }
  }

  public void testFileNameIsTheFileName() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      SegmentInfo si = writeCompoundSegment(dir);
      TreeSet<String> fileNames = new TreeSet<>();
      try (IndexInput in = dir.openInput(si.name + ".cfe", IOContext.READONCE)) {
        in.seek(CodecUtil.indexHeaderLength("Lucene90CompoundEntries", ""));
        for (int i = in.readVInt(); i > 0; i--) {
          fileNames.add(in.readString());
          in.readLong();
          in.readLong();
        }
      }
      try (CompoundDirectory cfs = si.getCodec().compoundFormat().getCompoundReader(dir, si)) {
        // javadoc: FileName is "that file's name"; written: the name without the segment name
        assertEquals("FileName values", new TreeSet<>(Arrays.asList(cfs.listAll())), fileNames);
      }
    }
  }
}
Command
cp TestLucene90CompoundEntriesJavadoc.java $LUCENE/lucene/core/src/test/org/apache/lucene/codecs/lucene90/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.codecs.lucene90.TestLucene90CompoundEntriesJavadoc -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
Expected
The documented grammar ends at the end of _0.cfe, and the FileName values are the compound file's names _0.fdm, _0.fdt, _0.fdx, _0.fnm.
Observed
bytes after the documented grammar expected:<0> but was:<16> and FileName values expected:<[_0.fdm, _0.fdt, _0.fdx, _0.fnm]> but was:<[.fdm, .fdt, .fdx, .fnm]>; the same with assertions off and on 10.6.0.

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

More files, environment, full output289 words
  • The test sits in the codec package, like Lucene's own codec tests; Lucene90CompoundEntries is the class's ENTRY_CODEC (:79). Both methods skip the IndexHeader, read FileCount and the entries exactly as the grammar says, then compare with the real file: the first checks that nothing is left, the second compares the names with the compound directory's listAll().
  • Two methods, because the javadoc makes two independent wrong statements; both read the same one-document segment.
  • Results: fails 2 of 2 on main with assertions on and off, standalone (2.59 s) and through gradle, and on the published 10.6.0 jars; 5 random-seed iterations fail 10 of 10 with the same messages. The hunt's and the verifiers' larger tests also fail on 10.5.2, 9.12.3 and the branch_10_6 head. On 9.x the test needs the 3-argument getCompoundReader(dir, si, IOContext.DEFAULT).
  • As a regression test: this test cannot guard the javadoc, because it transcribes the grammar. A form that pins down the corrected layout is this same test with its two assertions following the corrected grammar (CodecUtil.checkFooter after the last entry; names compared with stripSegmentName of listAll()), the fix check below.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); the same test also fails on the published 10.6.0 jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp TestLucene90CompoundEntriesJavadoc.java $LUCENE/lucene/core/src/test/org/apache/lucene/codecs/lucene90/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.codecs.lucene90.TestLucene90CompoundEntriesJavadoc -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)

Observed output

TestLucene90CompoundEntriesJavadoc > testFileNameIsTheFileName FAILED
    java.lang.AssertionError: FileName values expected:<[_0.fdm, _0.fdt, _0.fdx, _0.fnm]> but was:<[.fdm, .fdt, .fdx, .fnm]>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:F6E6066DC01DD8C4]:0)
        at org.junit.Assert.fail(Assert.java:89)
        at org.junit.Assert.failNotEquals(Assert.java:835)
        at org.junit.Assert.assertEquals(Assert.java:120)
        at org.apache.lucene.codecs.lucene90.TestLucene90CompoundEntriesJavadoc.testFileNameIsTheFileName(TestLucene90CompoundEntriesJavadoc.java:76)

TestLucene90CompoundEntriesJavadoc > testEntryTableEndsAfterLastEntry FAILED
    java.lang.AssertionError: bytes after the documented grammar expected:<0> but was:<16>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:DDD941FE5F8B8BCF]:0)
        at org.junit.Assert.fail(Assert.java:89)
        at org.junit.Assert.failNotEquals(Assert.java:835)
        at org.junit.Assert.assertEquals(Assert.java:647)
        at org.apache.lucene.codecs.lucene90.TestLucene90CompoundEntriesJavadoc.testEntryTableEndsAfterLastEntry(TestLucene90CompoundEntriesJavadoc.java:57)

2 tests completed, 2 failed

Why it looks unintended

  • The .cfs line right above lists its Footer; so does every explicit whole-file production in the core codecs but .liv.
  • 4.8.0-4.10.4 listed the .cfe Footer; a 2014 bulk backport replaced that line with trunk text that lacked it.
  • "That file's name" was true until 3.1 (LUCENE-2455), when the writer started stripping the segment name; the sentence was not updated.
The full argument177 words
  • The line above. .cfs --> Header, FileData^FileCount, Footer (:49), with its own Footer type line (:57); both files get a footer in the same method (:106-107).
  • Convention. Every other explicit whole-file production ((.ext) --> ...) in the core codecs names its Footer, for example the norms metadata and data files (Lucene90NormsFormat.java:46, :71), .tvd/.tvx (Lucene90TermVectorsFormat.java:82, :157), .fnm, .si, the terms dictionary, postings and segments_N; the only other production without it is .liv (LFV-970). Formats documented in prose or as file lists (doc values, points, stored fields, vectors) have no such production to compare.
  • History. 4.8.0-4.10.4 listed the Footer on the .cfe line; it was lost when LUCENE-5944 replaced the 4.x text with the trunk text, which had it on the .cfs line instead. No issue or review discusses it.
  • FileName. The sentence describes the pre-3.1 writer, which stored the full name; LUCENE-2455 changed the writer and not the sentence.
  • The contract. lucene104/package-info.java:50 and :188 (quoted under "Who is affected").
The devil's-advocate check304 words

Strongest case for "intended" (made by the refute reviewer, the intended-lens reviewer and the devil's-advocate verifier; the cross-model reviewers rated it moderate): the overview's History says "checksum footers were added to the end of each index file" since 4.8 (package-info.java:381-383), dev-docs/file-formats.md:80-84 says to add headers and footers to all files and TestAllFilesHaveChecksumFooter checks every file, .cfe included, so a per-file grammar need not repeat the footer; the overview calls itself "a high-level definition" (:54); the reader calls the stored string an id (Lucene90CompoundReader.java:144); and CompoundFormat is @lucene.experimental (CompoundFormat.java:28). The .cfs production is not byte-complete either: the 64-byte alignment padding between sub-files is only in the Notes prose. Under the generous footer reading the documented grammar parses all 87 .cfe files of the hunt's sweep.

Why it fails: under that reading the .cfs Footer on the line above would be redundant too, and the explicit whole-file productions in the core codecs do not leave the footer out; the explicit .cfs line invites a literal reading of the .cfe line. "High-level" allows leaving detail to prose, as the .cfs alignment is, not a production that ends 16 bytes early with no prose mentioning those bytes. The id reading conflicts with the plain words "that file's name", which date from when the full name was stored, and the FileName part does not depend on the footer convention at all. @lucene.experimental is about API stability. Committers edited this block twice (LUCENE-10019, #15341) and only touched the alignment note, so that is not an acceptance decision. The generous reading lowers the severity of the footer part; it does not make the text right. The final verdicts of all verifiers and the adjudication kept the defect (the cross-model review with medium confidence), with the intended argument rated moderate by the cross-model reviewers.

Suggested fix

Recommended A. Add the Footer, say what FileName holds, drop the unused Checksum

Javadoc only: add , Footer to the .cfe production, describe FileName as the file name without the segment name (for example .fnm for _0.fnm), and drop the unused Checksum from the type line (cleanup). Validated: the test rewritten to the corrected grammar passes, TestLucene90CompoundFormat passes and :lucene:core:check -x test is clean.

All options and trade-offs3 options ยท 880 words

Two ways to correct the text; A was validated. Backport: the javadoc lines are the same on branch_10_6 (2 lines higher), so the same text change applies there if wanted; main and branch_11x are the usual targets.

, Footer at the end of the .cfe production; the type line DataOffset,DataLength --> UInt64; FileName --> String, the file name without the segment name (for example .fnm for _0.fnm); and the same words in the Notes sentence. Javadoc only, one file, 7 lines in and 5 out; the Notes sentence is reflowed the way google-java-format wants it.

--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundFormat.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundFormat.java
@@ -48,11 +48,12 @@
  * <ul>
  *   <li>Compound (.cfs) --&gt; Header, FileData <sup>FileCount</sup>, Footer
  *   <li>Compound Entry Table (.cfe) --&gt; Header, FileCount, &lt;FileName, DataOffset,
- *       DataLength&gt; <sup>FileCount</sup>
+ *       DataLength&gt; <sup>FileCount</sup>, Footer
  *   <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}
  *   <li>FileCount --&gt; {@link DataOutput#writeVInt VInt}
- *   <li>DataOffset,DataLength,Checksum --&gt; {@link DataOutput#writeLong UInt64}
- *   <li>FileName --&gt; {@link DataOutput#writeString String}
+ *   <li>DataOffset,DataLength --&gt; {@link DataOutput#writeLong UInt64}
+ *   <li>FileName --&gt; {@link DataOutput#writeString String}, the file name without the segment
+ *       name (for example <code>.fnm</code> for <code>_0.fnm</code>)
  *   <li>FileData --&gt; raw file data
  *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}
  * </ul>
@@ -63,8 +64,9 @@
  *   <li>FileCount indicates how many files are contained in this compound file. The entry table
  *       that follows has that many entries.
  *   <li>Each directory entry contains a long pointer to the start of this file's data section, the
- *       files length, and a String with that file's name. The start of file's data section is
- *       aligned to 64 bytes to not introduce additional unaligned accesses with mmap.
+ *       files length, and a String with that file's name without the segment name. The start of
+ *       file's data section is aligned to 64 bytes to not introduce additional unaligned accesses
+ *       with mmap.
  * </ul>
  */
 public final class Lucene90CompoundFormat extends CompoundFormat {

Trade-offs

  • The corrected text was checked against real files: the writer's layout parses all 87 .cfe files of the hunt's 91 generated indexes, all 1720 entries hold the stripped name, and the corrected text, extracted from the patched javadoc, parses all 38 .cfe files of a second 41-configuration sweep.
  • Dropping Checksum is cleanup: the linked CodecFooter grammar defines it. Optional extra: the Notes list the entry parts as pointer, length, name, while the grammar order is FileName, DataOffset, DataLength; the sentence could follow the grammar order (not in the diff).

What the checks validated

Gradle in a scratch worktree of 645d4fe14a20 (seed DEADBEEF, 2026-11-01), two runs. Stock: the minimal repro fails 2 of 2 with assertions on and off. Run 2, the diff shown (fix A): the same test following the corrected grammar (CodecUtil.checkFooter after the last entry; names compared with stripSegmentName of listAll()) passes 2 of 2 with assertions on; TestLucene90CompoundFormat passes (36 tests, 0 failures, 1 nightly-only test skipped); :lucene:core:check -x test (google-java-format, ecj lint, forbidden APIs) succeeds. Run 1, on the hunt's text, which has the same grammar lines and differs only in the line breaks of the Notes sentence: the same test also passes with assertions off, TestLucene90CompoundFormat passes and :lucene:core:renderJavadoc renders the corrected grammar, but google-java-format failed on those line breaks, hence the reflowed diff. Option B passes google-java-format (run 2). The verbatim repro keeps failing with the fix, by design: it transcribes the old grammar, and the fix changes no code.

B. The two corrections only (not run)

, Footer on the .cfe line and the FileName wording (type line and note), keeping Checksum on the type line.

--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundFormat.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundFormat.java
@@ -48,11 +48,12 @@
  * <ul>
  *   <li>Compound (.cfs) --&gt; Header, FileData <sup>FileCount</sup>, Footer
  *   <li>Compound Entry Table (.cfe) --&gt; Header, FileCount, &lt;FileName, DataOffset,
- *       DataLength&gt; <sup>FileCount</sup>
+ *       DataLength&gt; <sup>FileCount</sup>, Footer
  *   <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}
  *   <li>FileCount --&gt; {@link DataOutput#writeVInt VInt}
  *   <li>DataOffset,DataLength,Checksum --&gt; {@link DataOutput#writeLong UInt64}
- *   <li>FileName --&gt; {@link DataOutput#writeString String}
+ *   <li>FileName --&gt; {@link DataOutput#writeString String}, the file name without the segment
+ *       name (for example <code>.fnm</code> for <code>_0.fnm</code>)
  *   <li>FileData --&gt; raw file data
  *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}
  * </ul>
@@ -63,8 +64,9 @@
  *   <li>FileCount indicates how many files are contained in this compound file. The entry table
  *       that follows has that many entries.
  *   <li>Each directory entry contains a long pointer to the start of this file's data section, the
- *       files length, and a String with that file's name. The start of file's data section is
- *       aligned to 64 bytes to not introduce additional unaligned accesses with mmap.
+ *       files length, and a String with that file's name without the segment name. The start of
+ *       file's data section is aligned to 64 bytes to not introduce additional unaligned accesses
+ *       with mmap.
  * </ul>
  */
 public final class Lucene90CompoundFormat extends CompoundFormat {

Trade-offs

Fixes both wrong statements and leaves the redundant Checksum entry for a reader to puzzle over.

What the checks validated

Not built or tested (the diff applies cleanly on 645d4fe14a20 and passes checkGoogleJavaFormat, run 2).

Leave the .cfe line as is, because the overview says every index file ends with a footer.

Trade-offs

Keeps the .cfe line inconsistent with the .cfs line beside it and does nothing for the FileName wording.

What the checks validated

Not run.

Downstream note

Not security-sensitive; it matters only to third-party tools that read or write .cfe files themselves from this javadoc, which would stop 16 bytes early or look up the wrong names.

How it was found

Semi-formal: a grammar check extracted the .cfe production and its type list from the javadoc and interpreted it over the bytes of 87 real .cfe files from 91 generated default-codec indexes, with each segment's compound directory as the oracle. All 87 ended 16 bytes early and all 1720 entries held the stripped name; the writer's real layout parsed all 87.

The method in full169 words
  • The check extracts the .cfe and .cfs productions and the shared type list mechanically from the javadoc source, checks them for well-formedness (it reports the unused definition Checksum) and interprets them over the real bytes of each compound segment. The oracle is the segment's CompoundDirectory: entry count, every FileName resolving to an inner file, DataLength equal to the inner file length, 64-byte aligned DataOffset.
  • Inputs: 91 indexes (11 fixed and 80 seeded random configurations), 87 compound segments, the same results with assertions on and off.
  • Positive control: the writer-layout grammar (the proposed fix text) parses all 87 .cfe files. Vacuity: 3 of 3 layout mutants (footer dropped, DataLength dropped, DataOffset/DataLength swapped) are killed. The .cfs production parses all 87 .cfs files. Fix check: the corrected text parses 38 of 38 files in a second sweep.
  • The unit's starting lead was a probe that saw 16 bytes after the documented grammar. This is a differential grammar check of documentation against real output, not model checking or a proof.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues, PRs and commits, the JIRA archive, the mailing lists, downstream projects or the web (searched 2026-10-28, rechecked 2026-11-01). Nearest: LUCENE-2446 (#3520), which made the line wrong, and LUCENE-2455 (#3529), which introduced the stripped names. Not fixed upstream.

Searches3 searches
  • Related, not this bug: #3520 (LUCENE-2446; no comment on the compound javadoc); #7006 (LUCENE-5944, the bulk backport); #7031 (LUCENE-5969) and #10744 / apache/lucene-solr#2268 (LUCENE-9705), which copied the text forward; #3529 (LUCENE-2455, the stripped names and why); #203 (LUCENE-10019) and #15341, which edited the alignment note of the same bullet and never mention the grammar.
  • Outside parsers, not reports: kaitai-io/kaitai_struct_formats#407 (a 2021 draft .cfe description that cites the Lucene50 javadoc but ends with a codec footer) and a 2018 gist that guards on end of file and handles leading-dot names. Neither says the javadoc is wrong or was reported to Lucene.
  • Not fixed upstream: the file is byte-identical on upstream main f815e679bf6c (2026-11-01) and branch_11x; branch_10_6 has the same javadoc lines. No open PR changes a compound file; the open javadoc PR #16949 touches no lucene90 file. Tracking issue #16937 does not name it.
What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene and apache/lucene-solr issues, PRs and commits; the JIRA archive; downstream repositories2026-10-28three scripted prior-report searches (footer, FileName, file-format docs: 72 issue/PR, 9 commit and 6 downstream queries); 43 more searches over apache/lucene, the main downstream repositories and the global index; the changed files of all 413 open PRs; the file's history on main, branch_10x and branch_10_6no report; only the commits that made or copied the text
Mailing lists, JIRA and web2026-10-28dev@, java-user@, issues@ and commits@ (60 scripted queries) and 12 scripted JIRA-archive queries; 6 more JIRA and 13 web searchesno report; two outside parsers that worked around the text
apache/lucene (recheck)2026-11-01the file's blob on upstream main, branch_11x and branch_10_6; its commits; issues and PRs updated since 2026-10-28 for 10 terms (Lucene90CompoundFormat, CompoundFormat javadoc, cfe footer, compound entry table, stripSegmentName, ...); the changed files of the 39 open PRs updated since then; #16949; #16937 and its commentsno report; the file is unchanged upstream

History

Footer part since 5.0.0: LUCENE-2446 (2014) put the Footer on the .cfs line, not the .cfe line, on trunk, and a bulk backport carried that text into 5.0. FileName part since 4.0.0. Affects 5.0.0 to 10.6.0 and main; 4.8.0-4.10.4 listed the .cfe Footer.

Versions tested7 rows
  • Origin (footer): c189d0fb74d (LUCENE-2446 "add checksums to index files", #3520, Robert Muir, 2014-04-01, trunk; JIRA patch era, no PR): writeFooter for both compound files, , Footer on the .cfs line only, Checksum on the type line. The branch_4x backport 819eda82e33 (2014-04-02) put , Footer on the .cfe line instead.
  • Released: 55594bad9b0 (LUCENE-5944, #7006, 2014-09-18), the branch_5x bulk backport of trunk, replaced the correct 4.x line: first affected release 5.0.0. Copied by LUCENE-5969 (af3e1d1c4b15 / 33bdf278cba, Lucene50CompoundFormat) and LUCENE-9705 (eafeb664340, apache/lucene-solr#2268, Lucene90CompoundFormat, 9.0.0).
  • FileName: stale since LUCENE-2455 (06b82e04ee9 / d6dd2e234e1, May 2010, 3.1); in the public format docs since 25a59d8902e / 49b3257d451 (2012-06-11), first release 4.0.0.
  • Later edits of the block (LUCENE-10019, #15341) changed only the alignment note.
Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails, both parts, with assertions on and off (standalone and gradle)
10.6.0 (published jars, 4ae946e5a9b) affectedthe minimal test fails, both parts
main a9779ef3c193, branch_10_6 head, 10.5.2, 9.12.3 affectedthe finder's and the independent reproduction's tests fail, both parts
5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 affectedthe history probe fails, both parts (each release's own documented grammar)
4.8.0, 4.10.4 partlyfooter part clean (the .cfe line lists the Footer); FileName part fails
4.0.0, 4.7.2 partlyfooter part clean (no footer written or documented); FileName part fails
main with the corrected text fixthe history probe's fixed-javadoc control passes

Independent checks

Confirmed by an independent reproducer, reviews by a reviewer from another model family (OpenAI GPT-5.6), an intended-behaviour devil's advocate and an adjudication (high confidence); a credibility audit confirmed it as a minor documentation defect. This doc re-ran the repro and validated the fix.

Checks and verdicts334 words

Every check either re-ran a repro on main or read the same lines; none found a reading under which the grammar is right as written.

Each independent check and its verdict
CheckVerdictWhat it found
cross-model review, refute lens (OpenAI GPT-5.6, executing) real, high confidenceIts own 4-test check failed the documented production and the FileName meaning, with 2 controls passing (one seed with assertions on, one off). Intended argument moderate; its correction (dropping Checksum is cleanup) was accepted.
independent reproducer realIts own test (3 documents) fails 2 of 3 on main (assertions on and off), 10.5.2 and 9.12.3 ("ends at byte 565 of 581 expected:<0> but was:<16>"; "FileName '.nvd' is not the name of a sub-file"); its control shows the 16 trailing bytes are exactly one valid codec footer.
cross-model review, intended lens (OpenAI GPT-5.6, reading only) real, medium confidenceIt could not run code, so it only read; a separate run re-ran the finder's test on main, failing as stated.
intended-behaviour devil's advocate real (documentation defect)Built and rejected the global-footer, "high-level", id and @lucene.experimental readings; corrected the history (footer part 5.0.0, not 4.8); re-ran on main and 10.5.2.
adjudication confirmed, high confidence, documentationRe-ran the independent reproduction's test and the finder's on new seeds and checked the 4.10.4 and 5.0.0 sources.
credibility audit confirmed, minorRe-ran the finder's test on a new seed; "Correct but trivial": a javadoc fix that would merge without fuss.
this doc's validation (2026-11-01) fix A validatedStock main: the minimal repro fails 2 of 2 with assertions on and off (gradle). With fix A (run 2): the test following the corrected grammar passes with assertions on, TestLucene90CompoundFormat passes (36 tests, 1 nightly-only skipped) and check -x test is clean. Run 1, on the hunt's text (the same grammar lines, other Notes line breaks): the same test also passes with assertions off and the javadoc renders; google-java-format failed on those line breaks, so the final diff is reflowed.

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,508 words

Finding record

LFV-971, snapshot of 2026-11-06

Registry title
The file-format javadoc of the default compound format (Lucene90CompoundFormat) gives an incomplete .cfe entry-table grammar: it leaves out the codec footer the writer appends (the .cfs line beside it lists one), and it says each entry holds the file's name although the writer stores the name with the segment name stripped (documentation defect; the writer and reader agree)
Area
codecs / compound files (Lucene90CompoundFormat .cfe file-format javadoc vs the Lucene90CompoundFormat writer / Lucene90CompoundReader)
Priority
Low, rank 1285: The .cfe grammar leaves out the footer, which the package-level footer rule partly covers, and misstates the stored names; it is documentation only.
Impact class
minor
Impact note
Documentation only: Lucene's writer and reader agree, so indexing, merging, CheckIndex and search are unaffected in every configuration; the people misled are those who read, inspect or re-implement the .cfe format from this class javadoc (the default codec's file-format overview links it as the Compound File format), and a parser that follows the text stops 16 bytes before the end of every .cfe file and takes stored names such as '.nvd' for full file names (executed); compound files are written by default (IndexWriterConfig.DEFAULT_USE_COMPOUND_FILE_SYSTEM = true), the footer has been missing from the grammar in every release since 5.0.0 and the FileName wording has been wrong since 4.0.0, and no scale or special setting is needed.
Summary
You decode the compound entry table (.cfe) of a default-codec segment by following the Lucene90CompoundFormat javadoc, which says the file is Header, FileCount and then FileCount entries of FileName, DataOffset, DataLength, each holding 'a String with that file's name'. The writer also appends a 16-byte codec footer (the .cfs line just above lists its Footer, the .cfe line does not) and stores each name with the segment name stripped, for example '.nvd' for _0.nvd, so a parser built from the text ends 16 bytes early (byte 134 of 150 in the minimal repro, byte 502 of 518 in the hunt's 20-document test) and cannot find the sub-files by the names it read. Lucene's own reader checks the footer and puts the segment name back, so nothing breaks at runtime; only the documentation is wrong (the type line also lists an unused 'Checksum', which belongs to the linked CodecFooter grammar, so dropping it is cleanup).
Introduced
Missing .cfe Footer (the main part): c189d0fb74dd1632c18d7c87d490516ff08b3fae (trunk, LUCENE-2446 'add checksums to index files', migrated as apache/lucene#3520; JIRA patch era, no PR) added CodecUtil.writeFooter to the .cfe writer and ', Footer' to the .cfs line only (plus 'Checksum' to the type line); the branch_4x backport 819eda82e33 put the Footer on the .cfe line instead, so 4.8.0-4.10.4 documented it. It reached the release line with 55594bad9b07af90de4ab2a2fb0bac8e8c2b0b3c (branch_5x, LUCENE-5944 bulk backport of trunk, apache/lucene#7006), which replaced the 4.x text with the trunk text; carried forward by LUCENE-5969 (apache/lucene#7031, 5893b1da299, Lucene50CompoundFormat) and LUCENE-9705 (apache/lucene-solr#2268, eafeb664340, Lucene90CompoundFormat). FileName wording: the writer stores stripSegmentName(file) since LUCENE-2455 (d6dd2e234e1 trunk / 06b82e04ee9 branch_3x, 2010), after which the older 'that file's name' sentence was stale; 25a59d8902e / 49b3257d451 ('merge/cleanup outdated javadocs', 2012-06-11) moved it into the public CompoundFileDirectory javadoc that the format overview links [read: git show in the full-history clone; exec: history probe boundary 4.10.4 not affected / 5.0.0 affected for the footer part] (2014-04-01 (author date of c189d0fb74d on trunk); 2014-09-18 (author date of 55594bad9b0, the release-line commit); FileName part: 2010-05-26/27 (LUCENE-2455) and 2012-06-11 (moved into the public format docs))
First affected version
5.0.0 [exec] for the missing .cfe Footer (the last unaffected release is 4.10.4 [exec]: 4.8.0-4.10.4 list the Footer on the .cfe line; 4.0.0-4.7.2 wrote no footer, so their footer-less line was right); FileName wording 4.0.0 [exec] in the public format docs (3.1 in an internal class javadoc [read]); 9.0.0 [exec] is the first release of Lucene90CompoundFormat
Affected versions
every release from 5.0.0 through 10.6.0 (the branch_10_6 head 4ae946e5a9b) and main a9779ef3c193; FileName part also 4.0.0-4.10.4; not fixed upstream (main 645d4fe14a20 on 2026-10-28 has the same text) [exec: a version-independent history probe that parses a real .cfe with each release's own documented production fails the footer part on 5.0.0, 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 (-ea and -da) and passes it on 4.0.0, 4.7.2, 4.8.0 and 4.10.4, and fails the FileName part on all 13 releases and main; the same probe with the fixed javadoc text passes on main; the finder's JUnit fails 2 of 3 on main (-ea and -da), the branch_10_6 head, 10.5.2 and 9.12.3 (9.x API variant), with its writer-layout control passing; the independent reproduction's JUnit fails 2 of 3 on main (-ea and -da), 10.5.2 and 9.12.3; read: the releases in between from the tags; branch_10x and branch_10_6 text read on GitHub]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, 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 (searched 2026-10-28: three scripted prior-report searches (footer, FileName, file-format docs: 72 GitHub issue/PR queries, 9 commit queries, 6 downstream-repo queries, 60 mailing-list queries over dev@, java-user@, issues@ and commits@, 12 JIRA-archive queries), 43 more issue/PR searches over apache/lucene, the main downstream repositories and the global index, 6 JIRA and 13 web searches; all 413 open apache/lucene PRs checked by changed files, and the PRs updated since 2026-10-26; the file's history on main, branch_10x and branch_10_6 by author date). Nearest items, not the same bug: #3520 / LUCENE-2446 (the change that made the .cfe line wrong; no comment on the compound javadoc), #7031 / LUCENE-5969 and #10744 / LUCENE-9705 with apache/lucene-solr#2268 (copied the text forward), #3529 / LUCENE-2455 (introduced the stripped names), #203 / LUCENE-10019 and #15341 (edited the alignment note of the same bullet, merged 2026-01-08, never mention the grammar); outside .cfe parsers (a 2021 draft format description, kaitai-io/kaitai_struct_formats#407, and a 2018 gist) worked around the text without reporting it. Recheck (2026-10-28): 'Lucene90CompoundFormat', 'CompoundFormat javadoc', 'cfe footer', 'Compound Entry Table', 'compound file javadoc', 'stripSegmentName' and 'cfe javadoc' return only old unrelated items; none of the issues and PRs created since 2026-10-26 is about it; of the 50 open PRs updated since then only #16911 (deletes the whole branch_10x tree, including the backward-codecs Lucene50CompoundFormat; not a fix) and #16480 (a package-info History bullet) touch a matching file; upstream main 645d4fe14a20 still has the text, last changed by 25e381bb7a1a (#15341, author date 2026-01-08). Not fixed upstream.
Lucene commit tested
main 645d4fe14a20: the minimal repro with assertions on and off, standalone and through gradle, and a byte probe of the same index; the recommended fix in a scratch worktree (the same test following the corrected grammar, the closest compound-format suite and :lucene:core:check -x test; assertions off and :lucene:core:renderJavadoc on the hunt's text, which differs only in the line breaks of the Notes sentence). The minimal repro on the published 10.6.0 jars. The hunt's and the verifiers' tests on main a9779ef3c193 (the same file), 4ae946e5a9b (the 10.6.0 tag commit, then the branch_10_6 head), 10.5.2 and 9.12.3; the history probe on 13 releases from 4.0.0 to 10.5.2. Lucene90CompoundFormat.java is byte-identical on 645d4fe14a20, upstream main f815e679bf6c (2026-11-01) and branch_11x; the branch_10_6 head 91eaab4fb298 and 10.6.0 have the same javadoc, 2 lines higher (read).

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundFormat.java:49 ('.cfs --> Header, FileData^FileCount, Footer'), :50-51 ('.cfe --> Header, FileCount, <FileName, DataOffset, DataLength>^FileCount', no Footer), :54 ('DataOffset,DataLength,Checksum --> UInt64'), :55 (FileName --> String), :57 (Footer --> CodecFooter), :65-67 ('a String with that file's name'), :107 (CodecUtil.writeFooter(entries)), :117 (writeVInt FileCount), :153-155 (writeString(IndexFileNames.stripSegmentName(file)), writeLong offset, writeLong length); Lucene90CompoundReader.java:133 (CodecUtil.checkFooter of the entries), :144 (readString id), :171 and :235 (lookups by stripSegmentName); read 2026-10-28
  • lucene/core/src/java/org/apache/lucene/codecs/lucene104/package-info.java:54 ('a high-level definition'), :188 ('Details on each of these are provided in their linked pages'), :237 (links Lucene90CompoundFormat as 'Compound File'), :381-383 (since 4.8 checksum footers at the end of each index file); CodecUtil.java:397-404 (CodecFooter --> Magic,AlgorithmID,Checksum); index/IndexWriterConfig.java:99 (DEFAULT_USE_COMPOUND_FILE_SYSTEM = true); dev-docs/file-formats.md:80-82 (headers and footers on all files); lucene/core/src/test/org/apache/lucene/index/TestAllFilesHaveChecksumFooter.java
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundFormat.java (lines 49-67 the javadoc; 95-109 write; 113-157 writeCompoundFile; 102 header, 106-107 footers, 117 FileCount, 153-155 entry)
  • lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundReader.java (lines 133 checkFooter, 139-147 readMapping, 171 and 235 lookups by stripSegmentName, 222 listAll adds the segment name)
  • lucene/core/src/java/org/apache/lucene/index/IndexFileNames.java (lines 133-144 stripSegmentName)
  • lucene/core/src/java/org/apache/lucene/codecs/CodecUtil.java (line 396 CodecFooter grammar)
Who is affected
  • lucene/core/src/java/org/apache/lucene/codecs/lucene104/package-info.java (lines 50, 188, 195, 237)
  • lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java (lines 96-99)
  • lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundReader.java (line 133)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90CompoundFormat.java (lines 49, 57, 106-107), lucene90/Lucene90NormsFormat.java (46, 71), lucene90/Lucene90TermVectorsFormat.java (82, 157), CompoundFormat.java (28), lucene104/package-info.java (50, 54, 188, 381-383), lucene90/Lucene90CompoundReader.java (144)
  • dev-docs/file-formats.md (lines 80-84) and lucene/core/src/test/org/apache/lucene/index/TestAllFilesHaveChecksumFooter.java

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

Repro files

About the diagrams

Made from the code at 645d4fe14a20 and an executed probe of the repro's index, in parallel with this doc.

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.