From 4eb7f06f2573b928a41f8f22bcd31ac3582e6eba Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 26 Oct 2025 17:23:13 -0700 Subject: [PATCH 1/4] Fix #598: Separate END_OBJECT return from parser close in ProtobufParser Previously, ProtobufParser would call close() while simultaneously returning END_OBJECT at the top level, causing the parser to be in a closed state immediately after returning the final token. This created incompatibility with jackson-core issue #1438. Solution: - Added new parser state STATE_ROOT_END to separate the two actions - Parser now returns END_OBJECT in STATE_ROOT_END state - Parser only closes on the subsequent nextToken() call - Updated all methods: nextToken(), nextName(), nextName(SerializableString), nextNameMatch(), and _skipUnknownField() - Clear current name when transitioning to STATE_ROOT_END Also added CLAUDE.md documentation file for AI assistants working with this codebase. Tests: - Added ParserStateEndTest with 3 test methods verifying correct behavior - All 98 protobuf tests pass with no regressions --- CLAUDE.md | 188 ++++++++++++++++++ .../dataformat/protobuf/ProtobufParser.java | 35 +++- .../protobuf/ParserStateEndTest.java | 150 ++++++++++++++ 3 files changed, 364 insertions(+), 9 deletions(-) create mode 100644 CLAUDE.md create mode 100644 protobuf/src/test/java/tools/jackson/dataformat/protobuf/ParserStateEndTest.java diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..6d3482376 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,188 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Jackson Dataformats Binary is a multi-module Maven project providing binary format backends for Jackson. It includes support for CBOR, Smile, Avro, Protobuf, and Ion formats. This is the 3.x branch (Java 17+) - Active development happens on `3.x` branch. + +Current version: 3.0.2-SNAPSHOT + +## Build Commands + +All builds use the Maven Wrapper (`./mvnw`): + +```bash +# Build and run all tests +./mvnw verify + +# Build without tests +./mvnw -DskipTests install + +# Run tests only +./mvnw test + +# Build a specific module +./mvnw -pl cbor verify +./mvnw -pl smile verify +./mvnw -pl avro verify +./mvnw -pl protobuf verify +./mvnw -pl ion verify + +# Run a specific test class +./mvnw -Dtest=CBORFactoryPropertiesTest test -pl cbor + +# Run a specific test method +./mvnw -Dtest=CBORFactoryPropertiesTest#testFactoryDefaults test -pl cbor + +# Clean build +./mvnw clean verify + +# Code coverage +./mvnw test +# Coverage reports in each module's target/site/jacoco/jacoco.xml +``` + +Maven flags commonly used: +- `-B` - batch mode (non-interactive) +- `-q` - quiet output +- `-ff` - fail-fast +- `-ntp` - no transfer progress + +## Module Structure + +The project has 5 independent format modules under the parent POM: + +- **cbor/** - CBOR (Concise Binary Object Representation) format support +- **smile/** - Smile format support (Jackson's own binary format) +- **avro/** - Apache Avro format support +- **protobuf/** - Protocol Buffers format support +- **ion/** - Amazon Ion format support + +## Architecture Patterns + +All modules follow a consistent architecture pattern: + +### Core Class Hierarchy + +Each module implements three core types that extend Jackson abstractions: + +1. **Factory** (extends `BinaryTSFactory` or `DecorableTSFactory`) + - Examples: `CBORFactory`, `SmileFactory`, `AvroFactory` + - Creates Parser and Generator instances + - Manages format-specific features via bitfield flags + - Key methods: `_createParser()`, `_createGenerator()`, `getFormatName()` + +2. **Parser** (extends `ParserBase`) + - Examples: `CBORParser`, `SmileParser`, `AvroParser` + - Implements binary format-specific parsing logic + - Uses context classes for state management + +3. **Generator** (extends `GeneratorBase`) + - Examples: `CBORGenerator`, `SmileGenerator`, `AvroGenerator` + - Implements binary encoding with output buffering + +### Builder Pattern + +Each factory has a companion builder: + +- **FactoryBuilder** (extends `DecorableTSFBuilder`) + - Examples: `CBORFactoryBuilder`, `SmileFactoryBuilder` + - Provides fluent API for configuration + - Methods: `enable(Feature)`, `disable(Feature)`, `configure(Feature, boolean)` + +### ObjectMapper Subclasses + +Each format provides a specialized mapper: + +- **Mapper** (extends `ObjectMapper`) + - Examples: `CBORMapper`, `SmileMapper`, `AvroMapper` + - Includes nested `Builder` class for fluent construction + - Static methods: `builder()`, `shared()` (singleton via `SharedWrapper`) + +### Feature Management + +Format-specific features are defined as enums implementing `FormatFeature`: + +- **ReadFeature** - Parser configuration (e.g., `CBORReadFeature`, `SmileReadFeature`) +- **WriteFeature** - Generator configuration (e.g., `CBORWriteFeature`, `SmileWriteFeature`) + +Features use bitfield operations for efficient storage and checking. + +### State Management + +Most modules use context classes for tracking parse/write state: + +- **ReadContext** (extends `TokenStreamContext`) - tracks parsing state +- **WriteContext** (extends `TokenStreamContext`) - tracks generation state + +Both support context reuse patterns for performance. + +### Format-Specific Extensions + +- **Avro**: Includes schema generation via `AvroSchemaGenerator`, requires `AvroModule`, has `AvroAnnotationIntrospector` +- **Protobuf**: Schema loading/generation, descriptor handling +- **Ion**: Supports both textual and binary output, delegates to Amazon's IonSystem +- **Smile**: Supports async non-blocking parsing via `NonBlockingByteArrayParser` +- **CBOR**: Supports tags and simple values (`CBORSimpleValue`) + +## Test Structure + +Tests follow a consistent pattern across modules: + +### Base Classes + +Each module has a base test class (e.g., `CBORTestBase`, `BaseTestForSmile`): +- Contains shared sample data (JSON test documents) +- Provides factory methods: `cborParser()`, `cborGenerator()`, `cborMapper()` +- Shared assertion helpers +- All tests extend this base class + +### Test Organization + +Tests are organized by category in subdirectories: + +- `parse/` - Parser-specific tests +- `gen/` - Generator-specific tests +- `mapper/` - ObjectMapper integration tests +- `constraints/` - Resource limit tests +- `dos/` - Denial-of-service protection tests +- `fuzz/` - Fuzz testing results +- `seq/` - Sequence reading/writing tests +- `testutil/` - Test utilities +- `tofix/` - Known issues/planned fixes + +Tests use JUnit 5 (project migrated from JUnit 4 in January 2025). + +## Common Utilities + +- **ByteQuadsCanonicalizer** - Shared symbol table for efficient string name handling +- **Bootstrapper Pattern** - Some formats use bootstrapper classes to defer parser instantiation (e.g., `CBORParserBootstrapper`, `SmileParserBootstrapper`) +- **Constants Classes** - Each format has constants (e.g., `CBORConstants`, `SmileConstants`) + +## Package Version Generation + +Each module uses maven-replacer-plugin to generate `PackageVersion.java` from `PackageVersion.java.in` template during build. + +## Java Version + +- This branch (3.x): Java 17+ +- Uses Jackson 3.x core libraries +- Parent POM: `tools.jackson:jackson-base:3.0.2-SNAPSHOT` + +## Dependencies + +All modules depend on: +- `com.fasterxml.jackson.core:jackson-annotations` +- `tools.jackson.core:jackson-core` +- `tools.jackson.core:jackson-databind` + +Format-specific modules may have additional dependencies (e.g., Avro uses Apache Avro libraries, Ion uses amazon-ion-java). + +## Key Implementation Notes + +1. **Immutability**: Factories are immutable - `snapshot()` returns `this`, `copy()` creates new instances +2. **Symbol Management**: All formats use `ByteQuadsCanonicalizer` for string symbol tables +3. **Feature Flags**: Bitfield-based feature management with mask operations +4. **Memory Limits**: Tests run with `-Xmx1024m` to catch oversized allocations +5. **Schema Support**: Avro and Protobuf require FormatSchema, others don't (`canUseSchema()` returns false) 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 750382b33..536f383d6 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java @@ -54,6 +54,10 @@ public class ProtobufParser extends ParserMinimalBase // State after either reaching end-of-input, or getting explicitly closed private final static int STATE_CLOSED = 12; + // State after returning END_OBJECT for root level, before closing + // (added for issue #598 to separate END_OBJECT return from close()) + private final static int STATE_ROOT_END = 13; + private final static int[] UTF8_UNIT_CODES = ProtobufUtil.sUtf8UnitLengths; // @since 2.14 @@ -539,7 +543,8 @@ public JsonToken nextToken() throws JacksonException // end-of-input? if (_inputPtr >= _inputEnd) { if (!loadMore()) { - close(); + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); return _updateToken(JsonToken.END_OBJECT); } } @@ -634,9 +639,14 @@ public JsonToken nextToken() throws JacksonException return _updateToken(_readNextValue(_currentField.type, STATE_NESTED_KEY)); case STATE_MESSAGE_END: // occurs if we end with array - close(); // sets state to STATE_CLOSED + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); return _updateToken(JsonToken.END_OBJECT); + case STATE_ROOT_END: // returned END_OBJECT, now close on next call + close(); // sets state to STATE_CLOSED + return null; + case STATE_CLOSED: return null; @@ -913,7 +923,8 @@ private JsonToken _skipUnknownField(int tag, int wireType, boolean nestedField) } } else if (_inputPtr >= _inputEnd) { if (!loadMore()) { - close(); + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); return _updateToken(JsonToken.END_OBJECT); } } @@ -976,7 +987,8 @@ public String nextName() throws JacksonException if (_state == STATE_ROOT_KEY) { if (_inputPtr >= _inputEnd) { if (!loadMore()) { - close(); + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); _updateToken(JsonToken.END_OBJECT); return null; } @@ -1051,7 +1063,8 @@ public String nextName() throws JacksonException return name; } if (_state == STATE_MESSAGE_END) { - close(); // sets state to STATE_CLOSED + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); _updateToken(JsonToken.END_OBJECT); return null; } @@ -1064,7 +1077,8 @@ public boolean nextName(SerializableString sstr) throws JacksonException if (_state == STATE_ROOT_KEY) { if (_inputPtr >= _inputEnd) { if (!loadMore()) { - close(); + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); _updateToken(JsonToken.END_OBJECT); return false; } @@ -1137,7 +1151,8 @@ public boolean nextName(SerializableString sstr) throws JacksonException return name.equals(sstr.getValue()); } if (_state == STATE_MESSAGE_END) { - close(); // sets state to STATE_CLOSED + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); _updateToken(JsonToken.END_OBJECT); return false; } @@ -1150,7 +1165,8 @@ public int nextNameMatch(PropertyNameMatcher matcher) throws JacksonException if (_state == STATE_ROOT_KEY) { if (_inputPtr >= _inputEnd) { if (!loadMore()) { - close(); + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); _updateToken(JsonToken.END_OBJECT); return PropertyNameMatcher.MATCH_END_OBJECT; } @@ -1227,7 +1243,8 @@ public int nextNameMatch(PropertyNameMatcher matcher) throws JacksonException return matcher.matchName(name); } if (_state == STATE_MESSAGE_END) { - close(); // sets state to STATE_CLOSED + _state = STATE_ROOT_END; + _streamReadContext.setCurrentName(null); _updateToken(JsonToken.END_OBJECT); return PropertyNameMatcher.MATCH_END_OBJECT; } diff --git a/protobuf/src/test/java/tools/jackson/dataformat/protobuf/ParserStateEndTest.java b/protobuf/src/test/java/tools/jackson/dataformat/protobuf/ParserStateEndTest.java new file mode 100644 index 000000000..3dd9e6da1 --- /dev/null +++ b/protobuf/src/test/java/tools/jackson/dataformat/protobuf/ParserStateEndTest.java @@ -0,0 +1,150 @@ +package tools.jackson.dataformat.protobuf; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.*; + +import tools.jackson.dataformat.protobuf.schema.ProtobufSchema; +import tools.jackson.dataformat.protobuf.schema.ProtobufSchemaLoader; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for issue #598: Protobuf parser state handling wrong for implicit close (END_OBJECT) + */ +public class ParserStateEndTest extends ProtobufTestBase +{ + private final ProtobufMapper MAPPER = newObjectMapper(); + + /** + * Test that verifies the parser properly handles the end-of-input state. + * The parser should NOT be closed when returning the final END_OBJECT token; + * it should only be closed on the subsequent nextToken() call. + */ + @Test + public void testParserStateAtEndObject() throws Exception + { + // Use a simple Point schema + ProtobufSchema schema = ProtobufSchemaLoader.std.parse(PROTOC_POINT); + + // Create test data + Point input = new Point(42, 13); + byte[] bytes = MAPPER.writerFor(Point.class) + .with(schema) + .writeValueAsBytes(input); + + // Parse with streaming parser + try (JsonParser p = MAPPER.reader() + .with(schema) + .createParser(bytes)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + // First field: "x" + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("x", p.currentName()); + + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(42, p.getIntValue()); + + // Second field: "y" + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("y", p.currentName()); + + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(13, p.getIntValue()); + + // END_OBJECT - This is the critical test + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + // THIS IS THE KEY ASSERTION: Parser should NOT be closed yet + // The parser should only be closed on the NEXT nextToken() call + assertFalse(p.isClosed(), + "Parser should NOT be closed immediately after returning END_OBJECT"); + + // Verify currentToken() returns END_OBJECT (not null) + assertEquals(JsonToken.END_OBJECT, p.currentToken(), + "currentToken() should return END_OBJECT, not null"); + + // Now the next token should be null AND close the parser + assertNull(p.nextToken(), "After END_OBJECT, nextToken() should return null"); + assertTrue(p.isClosed(), "Parser should be closed after nextToken() returns null"); + } + } + + /** + * Similar test but using nextName() optimization + */ + @Test + public void testParserStateAtEndObjectWithNextName() throws Exception + { + ProtobufSchema schema = ProtobufSchemaLoader.std.parse(PROTOC_POINT); + + Point input = new Point(42, 13); + byte[] bytes = MAPPER.writerFor(Point.class) + .with(schema) + .writeValueAsBytes(input); + + try (JsonParser p = MAPPER.reader() + .with(schema) + .createParser(bytes)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + // Use nextName() for field access + assertEquals("x", p.nextName()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + + assertEquals("y", p.nextName()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + + // Should get null from nextName() at end + assertNull(p.nextName()); + + // Current token should be END_OBJECT + assertEquals(JsonToken.END_OBJECT, p.currentToken(), + "currentToken() should return END_OBJECT after nextName() returns null"); + + // Parser should NOT be closed yet + assertFalse(p.isClosed(), + "Parser should NOT be closed when currentToken is END_OBJECT"); + + // Next token should be null and close parser + assertNull(p.nextToken()); + assertTrue(p.isClosed()); + } + } + + /** + * Test with empty message (no fields) + */ + @Test + public void testParserStateWithEmptyMessage() throws Exception + { + final String PROTOC_EMPTY = "message Empty {}\n"; + ProtobufSchema schema = ProtobufSchemaLoader.std.parse(PROTOC_EMPTY); + + // Empty message = just START_OBJECT, END_OBJECT + byte[] bytes = MAPPER.writer() + .with(schema) + .writeValueAsBytes(new Object()); + + try (JsonParser p = MAPPER.reader() + .with(schema) + .createParser(bytes)) { + // START_OBJECT + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertFalse(p.isClosed()); + + // END_OBJECT immediately + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + // Parser should NOT be closed yet + assertFalse(p.isClosed(), + "Parser should NOT be closed immediately after END_OBJECT"); + assertEquals(JsonToken.END_OBJECT, p.currentToken()); + + // Next token closes + assertNull(p.nextToken()); + assertTrue(p.isClosed()); + } + } +} From be3228a1c46efaf130b857df657e2dfce7de5666 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 26 Oct 2025 17:25:09 -0700 Subject: [PATCH 2/4] Rm accidentally added file --- CLAUDE.md | 188 ------------------------------------------------------ 1 file changed, 188 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 6d3482376..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,188 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Jackson Dataformats Binary is a multi-module Maven project providing binary format backends for Jackson. It includes support for CBOR, Smile, Avro, Protobuf, and Ion formats. This is the 3.x branch (Java 17+) - Active development happens on `3.x` branch. - -Current version: 3.0.2-SNAPSHOT - -## Build Commands - -All builds use the Maven Wrapper (`./mvnw`): - -```bash -# Build and run all tests -./mvnw verify - -# Build without tests -./mvnw -DskipTests install - -# Run tests only -./mvnw test - -# Build a specific module -./mvnw -pl cbor verify -./mvnw -pl smile verify -./mvnw -pl avro verify -./mvnw -pl protobuf verify -./mvnw -pl ion verify - -# Run a specific test class -./mvnw -Dtest=CBORFactoryPropertiesTest test -pl cbor - -# Run a specific test method -./mvnw -Dtest=CBORFactoryPropertiesTest#testFactoryDefaults test -pl cbor - -# Clean build -./mvnw clean verify - -# Code coverage -./mvnw test -# Coverage reports in each module's target/site/jacoco/jacoco.xml -``` - -Maven flags commonly used: -- `-B` - batch mode (non-interactive) -- `-q` - quiet output -- `-ff` - fail-fast -- `-ntp` - no transfer progress - -## Module Structure - -The project has 5 independent format modules under the parent POM: - -- **cbor/** - CBOR (Concise Binary Object Representation) format support -- **smile/** - Smile format support (Jackson's own binary format) -- **avro/** - Apache Avro format support -- **protobuf/** - Protocol Buffers format support -- **ion/** - Amazon Ion format support - -## Architecture Patterns - -All modules follow a consistent architecture pattern: - -### Core Class Hierarchy - -Each module implements three core types that extend Jackson abstractions: - -1. **Factory** (extends `BinaryTSFactory` or `DecorableTSFactory`) - - Examples: `CBORFactory`, `SmileFactory`, `AvroFactory` - - Creates Parser and Generator instances - - Manages format-specific features via bitfield flags - - Key methods: `_createParser()`, `_createGenerator()`, `getFormatName()` - -2. **Parser** (extends `ParserBase`) - - Examples: `CBORParser`, `SmileParser`, `AvroParser` - - Implements binary format-specific parsing logic - - Uses context classes for state management - -3. **Generator** (extends `GeneratorBase`) - - Examples: `CBORGenerator`, `SmileGenerator`, `AvroGenerator` - - Implements binary encoding with output buffering - -### Builder Pattern - -Each factory has a companion builder: - -- **FactoryBuilder** (extends `DecorableTSFBuilder`) - - Examples: `CBORFactoryBuilder`, `SmileFactoryBuilder` - - Provides fluent API for configuration - - Methods: `enable(Feature)`, `disable(Feature)`, `configure(Feature, boolean)` - -### ObjectMapper Subclasses - -Each format provides a specialized mapper: - -- **Mapper** (extends `ObjectMapper`) - - Examples: `CBORMapper`, `SmileMapper`, `AvroMapper` - - Includes nested `Builder` class for fluent construction - - Static methods: `builder()`, `shared()` (singleton via `SharedWrapper`) - -### Feature Management - -Format-specific features are defined as enums implementing `FormatFeature`: - -- **ReadFeature** - Parser configuration (e.g., `CBORReadFeature`, `SmileReadFeature`) -- **WriteFeature** - Generator configuration (e.g., `CBORWriteFeature`, `SmileWriteFeature`) - -Features use bitfield operations for efficient storage and checking. - -### State Management - -Most modules use context classes for tracking parse/write state: - -- **ReadContext** (extends `TokenStreamContext`) - tracks parsing state -- **WriteContext** (extends `TokenStreamContext`) - tracks generation state - -Both support context reuse patterns for performance. - -### Format-Specific Extensions - -- **Avro**: Includes schema generation via `AvroSchemaGenerator`, requires `AvroModule`, has `AvroAnnotationIntrospector` -- **Protobuf**: Schema loading/generation, descriptor handling -- **Ion**: Supports both textual and binary output, delegates to Amazon's IonSystem -- **Smile**: Supports async non-blocking parsing via `NonBlockingByteArrayParser` -- **CBOR**: Supports tags and simple values (`CBORSimpleValue`) - -## Test Structure - -Tests follow a consistent pattern across modules: - -### Base Classes - -Each module has a base test class (e.g., `CBORTestBase`, `BaseTestForSmile`): -- Contains shared sample data (JSON test documents) -- Provides factory methods: `cborParser()`, `cborGenerator()`, `cborMapper()` -- Shared assertion helpers -- All tests extend this base class - -### Test Organization - -Tests are organized by category in subdirectories: - -- `parse/` - Parser-specific tests -- `gen/` - Generator-specific tests -- `mapper/` - ObjectMapper integration tests -- `constraints/` - Resource limit tests -- `dos/` - Denial-of-service protection tests -- `fuzz/` - Fuzz testing results -- `seq/` - Sequence reading/writing tests -- `testutil/` - Test utilities -- `tofix/` - Known issues/planned fixes - -Tests use JUnit 5 (project migrated from JUnit 4 in January 2025). - -## Common Utilities - -- **ByteQuadsCanonicalizer** - Shared symbol table for efficient string name handling -- **Bootstrapper Pattern** - Some formats use bootstrapper classes to defer parser instantiation (e.g., `CBORParserBootstrapper`, `SmileParserBootstrapper`) -- **Constants Classes** - Each format has constants (e.g., `CBORConstants`, `SmileConstants`) - -## Package Version Generation - -Each module uses maven-replacer-plugin to generate `PackageVersion.java` from `PackageVersion.java.in` template during build. - -## Java Version - -- This branch (3.x): Java 17+ -- Uses Jackson 3.x core libraries -- Parent POM: `tools.jackson:jackson-base:3.0.2-SNAPSHOT` - -## Dependencies - -All modules depend on: -- `com.fasterxml.jackson.core:jackson-annotations` -- `tools.jackson.core:jackson-core` -- `tools.jackson.core:jackson-databind` - -Format-specific modules may have additional dependencies (e.g., Avro uses Apache Avro libraries, Ion uses amazon-ion-java). - -## Key Implementation Notes - -1. **Immutability**: Factories are immutable - `snapshot()` returns `this`, `copy()` creates new instances -2. **Symbol Management**: All formats use `ByteQuadsCanonicalizer` for string symbol tables -3. **Feature Flags**: Bitfield-based feature management with mask operations -4. **Memory Limits**: Tests run with `-Xmx1024m` to catch oversized allocations -5. **Schema Support**: Avro and Protobuf require FormatSchema, others don't (`canUseSchema()` returns false) From f7f4a738312865421dc49db4d2a4a7dda34884a5 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 26 Oct 2025 17:29:14 -0700 Subject: [PATCH 3/4] ... --- .../java/tools/jackson/dataformat/protobuf/ProtobufParser.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 536f383d6..5a29bfc4a 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java @@ -56,11 +56,12 @@ public class ProtobufParser extends ParserMinimalBase // State after returning END_OBJECT for root level, before closing // (added for issue #598 to separate END_OBJECT return from close()) + // + // @since 3.1 private final static int STATE_ROOT_END = 13; private final static int[] UTF8_UNIT_CODES = ProtobufUtil.sUtf8UnitLengths; - // @since 2.14 protected final static JacksonFeatureSet PROTOBUF_READ_CAPABILITIES = DEFAULT_READ_CAPABILITIES.with(StreamReadCapability.EXACT_FLOATS); From 15b6cfa1b766f2464d756883d493bfc3e262cfe0 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 26 Oct 2025 17:31:33 -0700 Subject: [PATCH 4/4] Update release notes --- release-notes/VERSION | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-notes/VERSION b/release-notes/VERSION index 3239ad4ed..c9a80d207 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -16,6 +16,8 @@ implementations) 3.1.0 (not yet released) +#598: (protobuf) Protobuf parser state handling wrong for implicit close (END_OBJECT) + (fix by @cowtowncoder, w/ Claude code) #619: (avro, cbor, ion, smile) Add `isEnabled()` methods for format-specific features (like `CBORReadFeature` and `CBORWriteFeature`) to mappers (requested by Andy W)