Skip to content

fix(extractor): stream text/markdown/xml extraction with BOM and length cap#165

Merged
marevol merged 5 commits into
masterfrom
fix/extractor-text-streaming
Jul 5, 2026
Merged

fix(extractor): stream text/markdown/xml extraction with BOM and length cap#165
marevol merged 5 commits into
masterfrom
fix/extractor-text-streaming

Conversation

@marevol

@marevol marevol commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Detect UTF-8/UTF-16/UTF-32 BOMs via BOMInputStream and decode accordingly; fall back to configured encoding when absent.
  • Replace whole-file IOUtils.toString / new String(getBytes()) with BufferedReader streaming bounded by maxTextLength (default unlimited; configurable).
  • AbstractXmlExtractor now strips BOM bytes from the actual content stream before decoding so the XML parser does not see a leading .
  • MarkdownExtractor reuses the same Reader/BOM pipeline before handing the source to commonmark; YAML front-matter extraction is unaffected.

Why

Large text/markdown/XML files cause unnecessary heap pressure since the entire byte buffer was materialized as a String before processing. Non-UTF-8 BOMs were silently misdecoded with the configured encoding, and the leading BOM character sometimes leaked into the extracted content.

Tests

  • BOM variants for UTF-8 / UTF-16 LE / UTF-16 BE on TextExtractor, MarkdownExtractor, XmlExtractor.
  • Shift_JIS without BOM (configured encoding wins).
  • Truncation at maxTextLength.
  • Large-file (10 MiB) streaming verifies exact length and head/tail bytes.
  • Markdown body / YAML front matter / no-front-matter paths.
  • XML extractor BOM tests using existing extractor/xml/test_utf8bom.xml, test_utf16lebom.xml, test_utf16bebom.xml fixtures.
  • All existing extractor tests still pass.

Test plan

  • CI green
  • Manual review of BOM + reader composition

marevol added 5 commits May 5, 2026 07:57
…th cap

Replaces full byte-buffering (IOUtils.toString / new String(getBytes())) in
TextExtractor, MarkdownExtractor and AbstractXmlExtractor with Reader-based
streaming through a BufferedReader. Detects UTF-8 / UTF-16 LE/BE / UTF-32
LE/BE BOMs via BOMInputStream and decodes accordingly, falling back to the
configured encoding when no BOM is present. AbstractXmlExtractor also strips
BOM bytes from the actual content stream so the parser sees pure XML.

Adds a configurable maxTextLength on each extractor (default Long.MAX_VALUE,
i.e. unlimited) to bound heap usage on very large inputs and stop reading
early when reached.

Tests cover UTF-8/UTF-16 LE/UTF-16 BE BOM stripping, Shift_JIS without BOM,
truncation at maxTextLength, large-file (10 MiB) streaming, Markdown body
+ YAML front matter + BOM-prefixed Markdown, and XML BOM extraction. All
existing extractor tests still pass.
Address review feedback for #165.

- Emit truncated=true and maxTextLength metadata on ExtractData when
  maxTextLength clips the input, plus a WARN log; keep the partial body
  instead of throwing (which would mismatch the intent of a soft cap).
- Move BOMInputStream into try-with-resources in TextExtractor and
  MarkdownExtractor; replace the @SuppressWarnings("resource") block in
  AbstractXmlExtractor.getText so the BOM-stripping stream is closed
  through the same chain.
- Drop a trailing unpaired high surrogate at the truncation boundary so
  the returned string is always a valid Java UTF-16 sequence.
- Clarify JavaDoc for maxTextLength: char (UTF-16 code unit) basis,
  Long.MAX_VALUE described as "effectively unlimited", values <= 0
  explicitly disable the limit. Document that getText closes the stream
  and that truncation may break YAML front-matter recovery.
- Migrate AbstractXmlExtractor.getEncoding off the deprecated
  BOMInputStream constructor to the builder API for consistency.
- Strengthen existing XML BOM tests (assert real attribute values
  survive extractString) and add 19 tests covering UTF-32 BOMs,
  Shift_JIS Markdown without BOM, surrogate-pair boundary, mid-stream
  BOM passthrough, maxTextLength=0/-1 unlimited, exact-length and
  one-char caps, across-buffer-boundary truncation, the truncated
  metadata flag, and large XML streaming.
…actor

Extract the duplicated "read chars from a Reader up to maxTextLength, drop a
trailing unpaired high surrogate, log a WARN, flag truncated" loop from
TextExtractor, MarkdownExtractor and AbstractXmlExtractor into a single
AbstractExtractor#readWithLimit(Reader, long) helper returning a shared
TextReadResult. The loop logic is byte-for-byte identical to the previous
inline implementations, so extraction behavior is unchanged; this only
removes the triplication so future changes to the surrogate/limit handling
live in one place.

Also drops the now-unused per-class READ_BUFFER_SIZE constants and
TextExtractor's truncation-only logger. The truncation WARN now originates
from AbstractExtractor but still identifies the concrete extractor via
getClass().getSimpleName().
Resolve the AbstractExtractor.java conflict by keeping both sides' additive
members, which are independent and non-overlapping:
- this branch (PR #165): readWithLimit(Reader, long) + TextReadResult, the
  shared bounded text-streaming helper used by Text/Markdown/Xml extractors.
- master (PR #161): archive-defense helpers EXTRACTOR_DEPTH_KEY, maxArchiveDepth,
  getCurrentDepth/incrementDepth/checkDepth, isPathTraversal, addOneSaturating,
  copyBounded.

No logic was dropped from either side; imports were merged in canonical order.
…avadoc

The BufferedReader import was removed from TextExtractor and AbstractXmlExtractor
when the shared bounded-read loop moved to AbstractExtractor.readWithLimit, but the
class Javadoc still referenced {@link BufferedReader}. The strict attach-javadocs
build could not resolve the link and failed with "reference not found". Switch those
two references to {@code BufferedReader} so no import is required.
@marevol marevol merged commit 9bfa75d into master Jul 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant