From 06b236e93146ca2ab9b2c4c02ef9b7e9aa23be22 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 12 Sep 2026 21:10:28 +0100 Subject: [PATCH] Support StreamReadConstraints.maxDocumentLength in ProtobufParser (#783) Validate document length when reloading the input buffer for streaming input (loadMore() / _loadToHaveAtLeast()), and up front for fixed byte[] input in ProtobufFactory, matching what CBOR and Smile already do. Co-Authored-By: Claude Opus 5 (1M context) --- .../dataformat/protobuf/ProtobufFactory.java | 2 + .../dataformat/protobuf/ProtobufParser.java | 2 + .../dos/LongDocumentProtobufReadTest.java | 113 ++++++++++++++++++ release-notes/CREDITS | 3 + release-notes/VERSION | 2 + 5 files changed, 122 insertions(+) create mode 100644 protobuf/src/test/java/tools/jackson/dataformat/protobuf/dos/LongDocumentProtobufReadTest.java diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufFactory.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufFactory.java index 12783b526..e8c15ddcf 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufFactory.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufFactory.java @@ -163,6 +163,8 @@ protected ProtobufParser _createParser(ObjectReadContext readCtxt, IOContext ioC protected ProtobufParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, byte[] data, int offset, int len) { + // [core#1548] Validate doc length up front for fixed buffers + _streamReadConstraints.validateDocumentLength(len); return new ProtobufParser(readCtxt, ioCtxt, readCtxt.getStreamReadFeatures(_streamReadFeatures), (ProtobufSchema) readCtxt.getSchema(), diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java index fe0f6113b..85c6c6924 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java @@ -2575,6 +2575,7 @@ protected final boolean loadMore() throws JacksonException { if (_inputStream != null) { _currInputProcessed += _inputEnd; + _streamReadConstraints.validateDocumentLength(_currInputProcessed); int count; try { @@ -2618,6 +2619,7 @@ protected final void _loadToHaveAtLeast(int minAvailable) throws JacksonExceptio if (ptr > 0) { _currInputProcessed += ptr; + _streamReadConstraints.validateDocumentLength(_currInputProcessed); if (amount > 0) { System.arraycopy(_inputBuffer, ptr, _inputBuffer, 0, amount); } diff --git a/protobuf/src/test/java/tools/jackson/dataformat/protobuf/dos/LongDocumentProtobufReadTest.java b/protobuf/src/test/java/tools/jackson/dataformat/protobuf/dos/LongDocumentProtobufReadTest.java new file mode 100644 index 000000000..9bae887a0 --- /dev/null +++ b/protobuf/src/test/java/tools/jackson/dataformat/protobuf/dos/LongDocumentProtobufReadTest.java @@ -0,0 +1,113 @@ +package tools.jackson.dataformat.protobuf.dos; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.exc.StreamConstraintsException; + +import tools.jackson.databind.ObjectMapper; + +import tools.jackson.dataformat.protobuf.*; +import tools.jackson.dataformat.protobuf.schema.ProtobufSchema; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +// [dataformats-binary#783]: `StreamReadConstraints.maxDocumentLength` for Protobuf +public class LongDocumentProtobufReadTest extends ProtobufTestBase +{ + public static class Item { + public String id; + public int size; + public long stuff; + } + + public static class Items { + public List items = new ArrayList<>(); + } + + private final static int MAX_DOC_LEN = 50_000; + + private final ProtobufMapper MAPPER_VANILLA = newObjectMapper(); + + private final ProtobufMapper MAPPER_CONSTRAINED = new ProtobufMapper( + ProtobufFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxDocumentLength(MAX_DOC_LEN) + .build()) + .build()); + + private final ProtobufSchema ITEMS_SCHEMA; + { + try { + ITEMS_SCHEMA = MAPPER_VANILLA.generateSchemaFor(Items.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongDocumentConstraint() throws Exception + { + // Need a bit longer than minimum since checking is approximate, not exact + byte[] doc = createBigDoc(60_000); + _testLongDocumentConstraint(doc, true); + _testLongDocumentConstraint(doc, false); + } + + @Test + public void testLongDocumentNoConstraint() throws Exception + { + byte[] doc = createBigDoc(60_000); + try (JsonParser p = protobufParser(MAPPER_VANILLA, new ByteArrayInputStream(doc))) { + while (p.nextToken() != null) { } + } + try (JsonParser p = protobufParser(MAPPER_VANILLA, doc)) { + while (p.nextToken() != null) { } + } + } + + private void _testLongDocumentConstraint(byte[] doc, boolean stream) throws Exception + { + try (JsonParser p = stream + ? protobufParser(MAPPER_CONSTRAINED, new ByteArrayInputStream(doc)) + : protobufParser(MAPPER_CONSTRAINED, doc)) { + while (p.nextToken() != null) { } + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + final String msg = e.getMessage(); + assertTrue(msg.contains("Document length ("), "unexpected message: "+msg); + assertTrue(msg.contains("exceeds the maximum allowed ("+MAX_DOC_LEN), "unexpected message: "+msg); + } + } + + private byte[] createBigDoc(final int size) throws Exception + { + Items items = new Items(); + // Each Item is ~50 bytes encoded + for (int i = 0, len = size / 50 + 1; i < len; ++i) { + Item item = new Item(); + item.id = UUID.randomUUID().toString(); + item.size = i; + item.stuff = Long.MAX_VALUE; + items.items.add(item); + } + byte[] doc = MAPPER_VANILLA.writer(ITEMS_SCHEMA).writeValueAsBytes(items); + assertTrue(doc.length > size, "doc.length="+doc.length); + return doc; + } + + private JsonParser protobufParser(ObjectMapper mapper, byte[] doc) throws Exception { + return mapper.readerFor(Items.class).with(ITEMS_SCHEMA).createParser(doc); + } + + private JsonParser protobufParser(ObjectMapper mapper, ByteArrayInputStream doc) throws Exception { + return mapper.readerFor(Items.class).with(ITEMS_SCHEMA).createParser(doc); + } +} diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 6f5d21a88..ab93eaa31 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -76,3 +76,6 @@ PJ Fanning (@pjfanning) * Contributed #763: (protobuf) Use `VarHandle` for multi-byte primitive reads and writes in `ProtobufParser` / `ProtobufGenerator` (3.3.0) +* Contributed #783: (protobuf) Support `StreamReadConstraints.maxDocumentLength` + in `ProtobufParser` + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 34c5dfd62..27615dde1 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -38,6 +38,8 @@ implementations) #767: (smile) Use more efficient `String` construction wrt "Compact Strings" for "short" ASCII text values of async parser (fix by @cowtowncoder, w/ Claude code) +#783: (protobuf) Support `StreamReadConstraints.maxDocumentLength` in `ProtobufParser` + (contributed by @pjfanning) 3.2.3 (not yet released)