Skip to content

fix(extractor): bound Ps/Json/Pdf extractor memory usage#179

Merged
marevol merged 2 commits into
masterfrom
perf/extractor-memory-bounds
Jul 9, 2026
Merged

fix(extractor): bound Ps/Json/Pdf extractor memory usage#179
marevol merged 2 commits into
masterfrom
perf/extractor-memory-bounds

Conversation

@marevol

@marevol marevol commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Bound the heap usage of three extractors that previously read/accumulated their input without any limit, so a large or hostile PostScript, JSON, or PDF document can no longer drive unbounded memory growth. All new bounds follow the module's existing truncate-not-reject convention and default to unlimited, so normal inputs are unchanged.

Changes

  • PsExtractor — replaced new String(InputStreamUtil.getBytes(in), enc) (whole stream → byte[]String) with AbstractExtractor.readWithLimit(reader, maxTextLength), the same helper TextExtractor/MarkdownExtractor use. New configurable maxTextLength (default Long.MAX_VALUE).
  • JsonExtractor
    • Cap the accumulated output text with a new TextAccumulator, which also bounds object-field output (previously only arrays were bounded via maxArrayElements; a huge flat object could still produce unbounded output).
    • Pin the ObjectMapper StreamReadConstraints explicitly to Jackson's own defaults (maxNestingDepth=1000, maxStringLength=20_000_000, maxNumberLength=1000). This documents the parse-time DoS ceilings without tightening them below what a default ObjectMapper already enforces — so no legitimate JSON that parsed before is rejected.
  • PdfExtractor
    • Bound the extracted text with a BoundedTextWriter (caps the sum across the page text, annotations, and embedded documents).
    • Spool the input to a temp file and use PDFBox's file-backed Loader.loadPDF(File) instead of RandomAccessReadBuffer(InputStream) (which buffers the whole PDF in heap). The temp file is always deleted in a finally (via FileUtil.deleteInBackground), after the document is closed. The per-call extraction executor/timeout is unchanged.

All truncation is high-surrogate-safe and sets the truncated / maxTextLength ExtractData metadata, consistent with the existing readWithLimit behavior.

Testing

  • Per-extractor: oversized-input truncation (metadata asserted) and normal-fixture parity (byte-identical output).
  • JsonExtractorTest: a legitimately deep (500-level) JSON is not rejected and extracts content — this guards against re-tightening maxNestingDepth below Jackson's default (it fails if the limit is dropped back to a small value); plus a truly-pathological (2000-level) document is rejected.
  • PdfExtractorTest: a BoundedTextWriter unit test proving the buffer never exceeds the cap, and a temp-file-cleanup test.
  • Full fess-crawler module suite passes (a pre-existing timing-sensitive FtpClientTest flake under parallel load is unrelated — these changes do not touch FTP — and passes in isolation); mvn formatter:format / license:format / javadoc:jar clean.

marevol added 2 commits July 9, 2026 06:19
Prevent unbounded heap use when extracting large or hostile PostScript,
JSON, and PDF documents, following the module's existing truncate-not-reject
convention. All new bounds default to unlimited, so normal inputs are
unchanged.

- PsExtractor: read via AbstractExtractor.readWithLimit with a configurable
  maxTextLength (default unlimited) instead of slurping the whole stream into
  a byte[] and String.
- JsonExtractor: cap the accumulated output text with a new TextAccumulator
  (which also bounds object-field output, not just arrays), and pin the
  ObjectMapper StreamReadConstraints to Jackson's own defaults (nesting 1000,
  string 20_000_000, number 1000) so the parse-time DoS ceilings are explicit
  without rejecting any input a default ObjectMapper would accept.
- PdfExtractor: bound the extracted text with a BoundedTextWriter and spool
  the input to a temp file for PDFBox's file-backed loading instead of
  buffering the whole PDF in memory; the temp file is always cleaned up.

Add tests for truncation, normal-input parity, a legitimately deep
(500-level) JSON that must not be rejected, and PDF temp-file cleanup.
…ngth

Add a shared AbstractExtractor.limitInputStream(in, limit) helper and an
opt-in maxContentLength (default 0 = unlimited) to JsonExtractor and
PdfExtractor so oversized input is rejected with MaxLengthExceededException
before it can exhaust heap or disk:

- JsonExtractor: readTree() materializes the whole JsonNode tree, so
  bounding maxTextLength (output) alone did not cap the dominant
  allocation. Wrap the parse input so the tree is built from at most
  maxContentLength bytes. Also correct the class Javadoc: the pinned
  StreamReadConstraints match Jackson's own defaults and are not the
  memory guard.
- PdfExtractor: the input was spooled to a temp file unbounded, trading a
  heap-exhaustion vector for a disk-exhaustion one. Bound the spool with
  the same helper.

Defaults are unchanged (maxContentLength <= 0 returns the stream unwrapped),
so existing behavior is byte-for-byte identical. PsExtractor already bounds
its read and is untouched.

Add tests for limitInputStream (exactly-limit accepted, limit+1 rejected,
non-positive returns the same instance) and for the new reject/under-cap
paths in JsonExtractorTest and PdfExtractorTest.
@marevol marevol merged commit b364134 into master Jul 9, 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