Skip to content

Commit 4b08f73

Browse files
authored
Fix #598: Separate END_OBJECT return from parser close in ProtobufParser (#626)
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 Tests: - Added ParserStateEndTest with 3 test methods verifying correct behavior
1 parent ba02f63 commit 4b08f73

File tree

3 files changed

+180
-10
lines changed

3 files changed

+180
-10
lines changed

protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,14 @@ public class ProtobufParser extends ParserMinimalBase
5454
// State after either reaching end-of-input, or getting explicitly closed
5555
private final static int STATE_CLOSED = 12;
5656

57+
// State after returning END_OBJECT for root level, before closing
58+
// (added for issue #598 to separate END_OBJECT return from close())
59+
//
60+
// @since 3.1
61+
private final static int STATE_ROOT_END = 13;
62+
5763
private final static int[] UTF8_UNIT_CODES = ProtobufUtil.sUtf8UnitLengths;
5864

59-
// @since 2.14
6065
protected final static JacksonFeatureSet<StreamReadCapability> PROTOBUF_READ_CAPABILITIES
6166
= DEFAULT_READ_CAPABILITIES.with(StreamReadCapability.EXACT_FLOATS);
6267

@@ -539,7 +544,8 @@ public JsonToken nextToken() throws JacksonException
539544
// end-of-input?
540545
if (_inputPtr >= _inputEnd) {
541546
if (!loadMore()) {
542-
close();
547+
_state = STATE_ROOT_END;
548+
_streamReadContext.setCurrentName(null);
543549
return _updateToken(JsonToken.END_OBJECT);
544550
}
545551
}
@@ -634,9 +640,14 @@ public JsonToken nextToken() throws JacksonException
634640
return _updateToken(_readNextValue(_currentField.type, STATE_NESTED_KEY));
635641

636642
case STATE_MESSAGE_END: // occurs if we end with array
637-
close(); // sets state to STATE_CLOSED
643+
_state = STATE_ROOT_END;
644+
_streamReadContext.setCurrentName(null);
638645
return _updateToken(JsonToken.END_OBJECT);
639646

647+
case STATE_ROOT_END: // returned END_OBJECT, now close on next call
648+
close(); // sets state to STATE_CLOSED
649+
return null;
650+
640651
case STATE_CLOSED:
641652
return null;
642653

@@ -913,7 +924,8 @@ private JsonToken _skipUnknownField(int tag, int wireType, boolean nestedField)
913924
}
914925
} else if (_inputPtr >= _inputEnd) {
915926
if (!loadMore()) {
916-
close();
927+
_state = STATE_ROOT_END;
928+
_streamReadContext.setCurrentName(null);
917929
return _updateToken(JsonToken.END_OBJECT);
918930
}
919931
}
@@ -976,7 +988,8 @@ public String nextName() throws JacksonException
976988
if (_state == STATE_ROOT_KEY) {
977989
if (_inputPtr >= _inputEnd) {
978990
if (!loadMore()) {
979-
close();
991+
_state = STATE_ROOT_END;
992+
_streamReadContext.setCurrentName(null);
980993
_updateToken(JsonToken.END_OBJECT);
981994
return null;
982995
}
@@ -1051,7 +1064,8 @@ public String nextName() throws JacksonException
10511064
return name;
10521065
}
10531066
if (_state == STATE_MESSAGE_END) {
1054-
close(); // sets state to STATE_CLOSED
1067+
_state = STATE_ROOT_END;
1068+
_streamReadContext.setCurrentName(null);
10551069
_updateToken(JsonToken.END_OBJECT);
10561070
return null;
10571071
}
@@ -1064,7 +1078,8 @@ public boolean nextName(SerializableString sstr) throws JacksonException
10641078
if (_state == STATE_ROOT_KEY) {
10651079
if (_inputPtr >= _inputEnd) {
10661080
if (!loadMore()) {
1067-
close();
1081+
_state = STATE_ROOT_END;
1082+
_streamReadContext.setCurrentName(null);
10681083
_updateToken(JsonToken.END_OBJECT);
10691084
return false;
10701085
}
@@ -1137,7 +1152,8 @@ public boolean nextName(SerializableString sstr) throws JacksonException
11371152
return name.equals(sstr.getValue());
11381153
}
11391154
if (_state == STATE_MESSAGE_END) {
1140-
close(); // sets state to STATE_CLOSED
1155+
_state = STATE_ROOT_END;
1156+
_streamReadContext.setCurrentName(null);
11411157
_updateToken(JsonToken.END_OBJECT);
11421158
return false;
11431159
}
@@ -1150,7 +1166,8 @@ public int nextNameMatch(PropertyNameMatcher matcher) throws JacksonException
11501166
if (_state == STATE_ROOT_KEY) {
11511167
if (_inputPtr >= _inputEnd) {
11521168
if (!loadMore()) {
1153-
close();
1169+
_state = STATE_ROOT_END;
1170+
_streamReadContext.setCurrentName(null);
11541171
_updateToken(JsonToken.END_OBJECT);
11551172
return PropertyNameMatcher.MATCH_END_OBJECT;
11561173
}
@@ -1227,7 +1244,8 @@ public int nextNameMatch(PropertyNameMatcher matcher) throws JacksonException
12271244
return matcher.matchName(name);
12281245
}
12291246
if (_state == STATE_MESSAGE_END) {
1230-
close(); // sets state to STATE_CLOSED
1247+
_state = STATE_ROOT_END;
1248+
_streamReadContext.setCurrentName(null);
12311249
_updateToken(JsonToken.END_OBJECT);
12321250
return PropertyNameMatcher.MATCH_END_OBJECT;
12331251
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package tools.jackson.dataformat.protobuf;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import tools.jackson.core.*;
6+
7+
import tools.jackson.dataformat.protobuf.schema.ProtobufSchema;
8+
import tools.jackson.dataformat.protobuf.schema.ProtobufSchemaLoader;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
/**
13+
* Test for issue #598: Protobuf parser state handling wrong for implicit close (END_OBJECT)
14+
*/
15+
public class ParserStateEndTest extends ProtobufTestBase
16+
{
17+
private final ProtobufMapper MAPPER = newObjectMapper();
18+
19+
/**
20+
* Test that verifies the parser properly handles the end-of-input state.
21+
* The parser should NOT be closed when returning the final END_OBJECT token;
22+
* it should only be closed on the subsequent nextToken() call.
23+
*/
24+
@Test
25+
public void testParserStateAtEndObject() throws Exception
26+
{
27+
// Use a simple Point schema
28+
ProtobufSchema schema = ProtobufSchemaLoader.std.parse(PROTOC_POINT);
29+
30+
// Create test data
31+
Point input = new Point(42, 13);
32+
byte[] bytes = MAPPER.writerFor(Point.class)
33+
.with(schema)
34+
.writeValueAsBytes(input);
35+
36+
// Parse with streaming parser
37+
try (JsonParser p = MAPPER.reader()
38+
.with(schema)
39+
.createParser(bytes)) {
40+
assertToken(JsonToken.START_OBJECT, p.nextToken());
41+
42+
// First field: "x"
43+
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
44+
assertEquals("x", p.currentName());
45+
46+
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
47+
assertEquals(42, p.getIntValue());
48+
49+
// Second field: "y"
50+
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
51+
assertEquals("y", p.currentName());
52+
53+
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
54+
assertEquals(13, p.getIntValue());
55+
56+
// END_OBJECT - This is the critical test
57+
assertToken(JsonToken.END_OBJECT, p.nextToken());
58+
59+
// THIS IS THE KEY ASSERTION: Parser should NOT be closed yet
60+
// The parser should only be closed on the NEXT nextToken() call
61+
assertFalse(p.isClosed(),
62+
"Parser should NOT be closed immediately after returning END_OBJECT");
63+
64+
// Verify currentToken() returns END_OBJECT (not null)
65+
assertEquals(JsonToken.END_OBJECT, p.currentToken(),
66+
"currentToken() should return END_OBJECT, not null");
67+
68+
// Now the next token should be null AND close the parser
69+
assertNull(p.nextToken(), "After END_OBJECT, nextToken() should return null");
70+
assertTrue(p.isClosed(), "Parser should be closed after nextToken() returns null");
71+
}
72+
}
73+
74+
/**
75+
* Similar test but using nextName() optimization
76+
*/
77+
@Test
78+
public void testParserStateAtEndObjectWithNextName() throws Exception
79+
{
80+
ProtobufSchema schema = ProtobufSchemaLoader.std.parse(PROTOC_POINT);
81+
82+
Point input = new Point(42, 13);
83+
byte[] bytes = MAPPER.writerFor(Point.class)
84+
.with(schema)
85+
.writeValueAsBytes(input);
86+
87+
try (JsonParser p = MAPPER.reader()
88+
.with(schema)
89+
.createParser(bytes)) {
90+
assertToken(JsonToken.START_OBJECT, p.nextToken());
91+
92+
// Use nextName() for field access
93+
assertEquals("x", p.nextName());
94+
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
95+
96+
assertEquals("y", p.nextName());
97+
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
98+
99+
// Should get null from nextName() at end
100+
assertNull(p.nextName());
101+
102+
// Current token should be END_OBJECT
103+
assertEquals(JsonToken.END_OBJECT, p.currentToken(),
104+
"currentToken() should return END_OBJECT after nextName() returns null");
105+
106+
// Parser should NOT be closed yet
107+
assertFalse(p.isClosed(),
108+
"Parser should NOT be closed when currentToken is END_OBJECT");
109+
110+
// Next token should be null and close parser
111+
assertNull(p.nextToken());
112+
assertTrue(p.isClosed());
113+
}
114+
}
115+
116+
/**
117+
* Test with empty message (no fields)
118+
*/
119+
@Test
120+
public void testParserStateWithEmptyMessage() throws Exception
121+
{
122+
final String PROTOC_EMPTY = "message Empty {}\n";
123+
ProtobufSchema schema = ProtobufSchemaLoader.std.parse(PROTOC_EMPTY);
124+
125+
// Empty message = just START_OBJECT, END_OBJECT
126+
byte[] bytes = MAPPER.writer()
127+
.with(schema)
128+
.writeValueAsBytes(new Object());
129+
130+
try (JsonParser p = MAPPER.reader()
131+
.with(schema)
132+
.createParser(bytes)) {
133+
// START_OBJECT
134+
assertToken(JsonToken.START_OBJECT, p.nextToken());
135+
assertFalse(p.isClosed());
136+
137+
// END_OBJECT immediately
138+
assertToken(JsonToken.END_OBJECT, p.nextToken());
139+
140+
// Parser should NOT be closed yet
141+
assertFalse(p.isClosed(),
142+
"Parser should NOT be closed immediately after END_OBJECT");
143+
assertEquals(JsonToken.END_OBJECT, p.currentToken());
144+
145+
// Next token closes
146+
assertNull(p.nextToken());
147+
assertTrue(p.isClosed());
148+
}
149+
}
150+
}

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ implementations)
1616

1717
3.1.0 (not yet released)
1818

19+
#598: (protobuf) Protobuf parser state handling wrong for implicit close (END_OBJECT)
20+
(fix by @cowtowncoder, w/ Claude code)
1921
#619: (avro, cbor, ion, smile) Add `isEnabled()` methods for format-specific features
2022
(like `CBORReadFeature` and `CBORWriteFeature`) to mappers
2123
(requested by Andy W)

0 commit comments

Comments
 (0)