Skip to content

Commit fa60314

Browse files
committed
Merge branch '2.19' into 2.20
2 parents 6192fc4 + e3043e5 commit fa60314

3 files changed

Lines changed: 242 additions & 6 deletions

File tree

cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2427,7 +2427,7 @@ protected void _finishToken() throws IOException
24272427
return;
24282428
}
24292429
// If not enough space, need handling similar to chunked
2430-
_finishLongText(len);
2430+
_finishLongText(len, false);
24312431
}
24322432

24332433
/**
@@ -2469,7 +2469,7 @@ protected String _finishTextToken(int ch) throws IOException
24692469
return _finishShortText(len);
24702470
}
24712471
// If not enough space, need handling similar to chunked
2472-
return _finishLongText(len);
2472+
return _finishLongText(len, false);
24732473
}
24742474

24752475
private final String _finishShortText(int len) throws IOException
@@ -2574,12 +2574,21 @@ private final String _finishShortText(int len) throws IOException
25742574
return str;
25752575
}
25762576

2577-
private final String _finishLongText(int len) throws IOException
2577+
/**
2578+
* @param isName Whether content being decoded is that of an Object property
2579+
* name (and not a String value): if so, "stringref" bookkeeping is left to
2580+
* the caller ({@code _decodePropertyName()} / {@code nextFieldName()}), which
2581+
* already adds names to the reference table -- doing it here as well would
2582+
* give a single name two indexes
2583+
*
2584+
* @since 2.18.10
2585+
*/
2586+
private final String _finishLongText(int len, boolean isName) throws IOException
25782587
{
25792588
// 24-Jul-2026, tatu: [dataformats-binary#733] Need to check this before
25802589
// decoding: `len` is decremented by the loop below (down to -1)
25812590
StringRefList stringRefs = null;
2582-
if (!_stringRefs.empty() &&
2591+
if (!isName && !_stringRefs.empty() &&
25832592
shouldReferenceString(_stringRefs.peek().stringRefs.size(), len)) {
25842593
stringRefs = _stringRefs.peek();
25852594
}
@@ -3268,8 +3277,10 @@ private final String _decodeLongerName(int len) throws IOException
32683277
if ((_inputEnd - _inputPtr) < len) {
32693278
// or if not, could we read?
32703279
if (len >= _inputBuffer.length) {
3271-
// If not enough space, need handling similar to chunked
3272-
return _finishLongText(len);
3280+
// If not enough space, need handling similar to chunked.
3281+
// 24-Jul-2026, tatu: [dataformats-binary#736] `true` for "isName"
3282+
// since caller adds the name to "stringref" table itself
3283+
return _finishLongText(len, true);
32733284
}
32743285
_loadToHaveAtLeast(len);
32753286
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package com.fasterxml.jackson.dataformat.cbor;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.ByteArrayOutputStream;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import com.fasterxml.jackson.core.JsonParser;
9+
import com.fasterxml.jackson.core.JsonToken;
10+
import com.fasterxml.jackson.core.io.SerializedString;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
/**
15+
* Tests for [dataformats-binary#736]: Object property names too long to fit in the
16+
* input buffer are decoded by {@code CBORParser._finishLongText()}, which -- since
17+
* [dataformats-binary#733] made it add what it decodes to the "stringref" reference
18+
* table -- registered the name a second time, on top of the registration the name
19+
* paths already do. A single name then consumed two indexes, so every following
20+
* reference resolved to the wrong String.
21+
*<p>
22+
* Only reproduces when reading from an {@link java.io.InputStream}: with
23+
* {@code byte[]} input the whole document is already buffered, so
24+
* {@code _decodeLongerName()} never delegates to {@code _finishLongText()}.
25+
*/
26+
public class StringRef736Test extends CBORTestBase
27+
{
28+
// Longer than the default 8000 byte input buffer, but below the default
29+
// 50000 byte `maxNameLength`, so this is reachable with stock settings
30+
private final static int LONG_NAME_LENGTH = 9000;
31+
32+
private final static int MODE_NEXT_TOKEN = 1;
33+
private final static int MODE_NEXT_FIELD_NAME = 2;
34+
private final static int MODE_NEXT_FIELD_NAME_MATCH = 3;
35+
36+
/*
37+
/**********************************************************
38+
/* Test methods
39+
/**********************************************************
40+
*/
41+
42+
// Long name is entry #0 and "AAA" entry #1, so reference #1 must be "AAA".
43+
// If the name is registered twice it becomes #0 AND #1, and #1 wrongly
44+
// resolves to the name
45+
@Test
46+
public void testLongNameDoesNotConsumeTwoIndexes() throws Exception
47+
{
48+
final String longName = generateAsciiString(LONG_NAME_LENGTH);
49+
final byte[] doc = _longNameDoc(longName, 1);
50+
51+
_verifyAllModes(doc, longName, "AAA");
52+
}
53+
54+
// Conversely, the name must still be registered exactly once: reference #0
55+
// resolves to the name itself
56+
@Test
57+
public void testLongNameStillReferencedOnce() throws Exception
58+
{
59+
final String longName = generateAsciiString(LONG_NAME_LENGTH);
60+
final byte[] doc = _longNameDoc(longName, 0);
61+
62+
_verifyAllModes(doc, longName, longName);
63+
}
64+
65+
// Same, with a name needing real UTF-8 decoding, since the name is decoded
66+
// by a different code path than the ASCII-only case
67+
@Test
68+
public void testLongUnicodeNameDoesNotConsumeTwoIndexes() throws Exception
69+
{
70+
StringBuilder sb = new StringBuilder();
71+
while (sb.length() < LONG_NAME_LENGTH) {
72+
sb.append("Beyoncé über 中文 ");
73+
}
74+
final String longName = sb.toString();
75+
final byte[] doc = _longNameDoc(longName, 1);
76+
77+
_verifyAllModes(doc, longName, "AAA");
78+
}
79+
80+
// String VALUES that go through `_finishLongText()` must still be registered:
81+
// verifies the fix did not disable reference tracking for values too
82+
@Test
83+
public void testLongValueStillReferenced() throws Exception
84+
{
85+
final String longValue = generateAsciiString(LONG_NAME_LENGTH);
86+
87+
// tag(256) [ <longValue>, "AAA", tag(25) 0 ] => #0 is longValue
88+
ByteArrayOutputStream b = new ByteArrayOutputStream();
89+
b.write(0xD9); b.write(0x01); b.write(0x00);
90+
b.write(0x83); // Array, 3 elements
91+
_writeText(b, utf8Bytes(longValue));
92+
b.write(0x63); b.write('A'); b.write('A'); b.write('A');
93+
b.write(0xD8); b.write(0x19); b.write(0x00); // stringref #0
94+
final byte[] doc = b.toByteArray();
95+
96+
for (boolean stream : new boolean[] { false, true }) {
97+
try (JsonParser p = _parser(doc, stream)) {
98+
assertToken(JsonToken.START_ARRAY, p.nextToken());
99+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
100+
assertEquals(longValue, p.getText());
101+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
102+
assertEquals("AAA", p.getText());
103+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
104+
assertEquals(longValue, p.getText(),
105+
"Reference to long value, stream = "+stream);
106+
assertToken(JsonToken.END_ARRAY, p.nextToken());
107+
assertNull(p.nextToken());
108+
}
109+
}
110+
}
111+
112+
/*
113+
/**********************************************************
114+
/* Helper methods, document construction
115+
/**********************************************************
116+
*/
117+
118+
/**
119+
* Builds document
120+
*<pre>
121+
* tag(256) [ { &lt;longName&gt; : "AAA" }, tag(25) refIndex ]
122+
*</pre>
123+
* where a conformant encoder assigns {@code longName} index #0 and
124+
* {@code "AAA"} index #1.
125+
*/
126+
private byte[] _longNameDoc(String longName, int refIndex) throws Exception
127+
{
128+
ByteArrayOutputStream b = new ByteArrayOutputStream();
129+
b.write(0xD9); b.write(0x01); b.write(0x00); // tag 256, "stringref-namespace"
130+
b.write(0x82); // Array, 2 elements
131+
b.write(0xA1); // Object, 1 entry
132+
_writeText(b, utf8Bytes(longName));
133+
b.write(0x63); b.write('A'); b.write('A'); b.write('A');
134+
b.write(0xD8); b.write(0x19); b.write(refIndex); // tag 25, "stringref"
135+
return b.toByteArray();
136+
}
137+
138+
/**
139+
* Writes text using the <i>minimal</i> (canonical) length prefix, the way a
140+
* real encoder would: what is under test here is which entries take up a
141+
* reference index, not how lengths are encoded. Non-minimal prefixes are
142+
* {@code StringRef735Test}'s subject.
143+
*/
144+
private void _writeText(ByteArrayOutputStream b, byte[] raw) {
145+
final int len = raw.length;
146+
if (len <= 23) { // length in the type byte itself
147+
b.write(0x60 + len);
148+
} else if (len <= 0xFF) { // 1-byte length suffix
149+
b.write(0x78);
150+
b.write(len);
151+
} else if (len <= 0xFFFF) { // 2-byte length suffix
152+
b.write(0x79);
153+
b.write(len >> 8);
154+
b.write(len & 0xFF);
155+
} else { // 4-byte length suffix
156+
b.write(0x7A);
157+
b.write(len >> 24);
158+
b.write((len >> 16) & 0xFF);
159+
b.write((len >> 8) & 0xFF);
160+
b.write(len & 0xFF);
161+
}
162+
b.write(raw, 0, len);
163+
}
164+
165+
/*
166+
/**********************************************************
167+
/* Helper methods, verification
168+
/**********************************************************
169+
*/
170+
171+
private void _verifyAllModes(byte[] doc, String expName, String expRef)
172+
{
173+
assertAll(
174+
() -> _verifyRef(doc, expName, expRef, MODE_NEXT_TOKEN),
175+
() -> _verifyRef(doc, expName, expRef, MODE_NEXT_FIELD_NAME),
176+
() -> _verifyRef(doc, expName, expRef, MODE_NEXT_FIELD_NAME_MATCH));
177+
}
178+
179+
private void _verifyRef(byte[] doc, String expName, String expRef, int mode)
180+
throws Exception
181+
{
182+
// `byte[]` input keeps the whole name buffered and is the control case;
183+
// only `InputStream` input reaches `_finishLongText()`
184+
for (boolean stream : new boolean[] { false, true }) {
185+
final String desc = "(mode: "+mode+", stream: "+stream+")";
186+
try (JsonParser p = _parser(doc, stream)) {
187+
assertToken(JsonToken.START_ARRAY, p.nextToken());
188+
assertToken(JsonToken.START_OBJECT, p.nextToken());
189+
_advanceToName(p, expName, mode);
190+
assertToken(JsonToken.FIELD_NAME, p.currentToken());
191+
assertEquals(expName, p.currentName(), desc);
192+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
193+
assertEquals("AAA", p.getText(), desc);
194+
assertToken(JsonToken.END_OBJECT, p.nextToken());
195+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
196+
assertEquals(expRef, p.getText(), desc);
197+
assertToken(JsonToken.END_ARRAY, p.nextToken());
198+
assertNull(p.nextToken());
199+
}
200+
}
201+
}
202+
203+
private void _advanceToName(JsonParser p, String expName, int mode)
204+
throws Exception
205+
{
206+
switch (mode) {
207+
case MODE_NEXT_TOKEN:
208+
assertToken(JsonToken.FIELD_NAME, p.nextToken());
209+
break;
210+
case MODE_NEXT_FIELD_NAME:
211+
assertEquals(expName, p.nextFieldName());
212+
break;
213+
default:
214+
// Will not match (names this long never take the fast path), but
215+
// the name still gets decoded, which is what matters here
216+
assertFalse(p.nextFieldName(new SerializedString("zzz")));
217+
break;
218+
}
219+
}
220+
221+
private JsonParser _parser(byte[] doc, boolean stream) throws Exception {
222+
return stream ? cborParser(new ByteArrayInputStream(doc)) : cborParser(doc);
223+
}
224+
}

release-notes/VERSION-2.x

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ No changes since 2.19.1
111111
following references
112112
#735: (cbor) "stringref" property-name paths pass 5-bit length marker instead of
113113
actual length to `shouldReferenceString()`
114+
#736: (cbor) Long Object property names added to "stringref" reference table twice
114115

115116
2.18.9 (07-Jul-2026)
116117

0 commit comments

Comments
 (0)