Skip to content

Commit 3f89e3e

Browse files
committed
Merge branch '2.19' into 2.20
2 parents 45def54 + c2e41ab commit 3f89e3e

3 files changed

Lines changed: 230 additions & 0 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2571,6 +2571,8 @@ private final String _finishShortText(int len) throws IOException
25712571

25722572
private final String _finishLongText(int len) throws IOException
25732573
{
2574+
// 24-Jul-2026, tatu: [dataformats-binary#733] Need to check this before
2575+
// decoding: `len` is decremented by the loop below (down to -1)
25742576
StringRefList stringRefs = null;
25752577
if (!_stringRefs.empty() &&
25762578
shouldReferenceString(_stringRefs.peek().stringRefs.size(), len)) {
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNull;
14+
15+
/**
16+
* Tests for [dataformats-binary#733]: Strings decoded by
17+
* {@code CBORParser._finishLongText()} -- that is, Strings that do not fit
18+
* in the input buffer, only possible when reading from an {@link java.io.InputStream}
19+
* -- were not added to the String reference table. Since reference indexes are
20+
* positional, that made the parser's table disagree with the encoder's, so that
21+
* following references either failed with "String reference out of range" or,
22+
* worse, silently resolved to the wrong String.
23+
*/
24+
public class StringRef733Test extends CBORTestBase
25+
{
26+
// Longer than the default 8000 byte input buffer, to force use of
27+
// `_finishLongText()` when reading from an `InputStream`
28+
private final static int LONG_LENGTH = 9000;
29+
30+
/*
31+
/**********************************************************
32+
/* Test methods, hand-crafted documents
33+
/**********************************************************
34+
*/
35+
36+
// Long String is entry #0, so reference #1 must resolve to "AAA"
37+
@Test
38+
public void testLongStringDoesNotShiftFollowingRefs() throws Exception
39+
{
40+
final String longString = _generateAscii(LONG_LENGTH);
41+
final byte[] doc = _stringRefDoc(longString, 1);
42+
43+
_verifyRefResolvesTo(_parser(doc, false), longString, "AAA");
44+
_verifyRefResolvesTo(_parser(doc, true), longString, "AAA");
45+
}
46+
47+
// Reference #0 is the long String itself: before fix, failed with
48+
// "String reference (0) out of range" since table was left empty
49+
@Test
50+
public void testReferenceToLongStringItself() throws Exception
51+
{
52+
final String longString = _generateAscii(LONG_LENGTH);
53+
final byte[] doc = _stringRefDoc(longString, 0);
54+
55+
_verifyRefResolvesTo(_parser(doc, false), longString, longString);
56+
_verifyRefResolvesTo(_parser(doc, true), longString, longString);
57+
}
58+
59+
// Same, but with a String that needs actual UTF-8 decoding (not just the
60+
// ASCII path), to verify multi-byte characters do not throw the count off
61+
@Test
62+
public void testReferenceToLongUnicodeString() throws Exception
63+
{
64+
StringBuilder sb = new StringBuilder();
65+
while (sb.length() < LONG_LENGTH) {
66+
sb.append("Beyoncé über 中文 ");
67+
}
68+
final String longString = sb.toString();
69+
final byte[] doc = _stringRefDoc(longString, 0);
70+
71+
_verifyRefResolvesTo(_parser(doc, false), longString, longString);
72+
_verifyRefResolvesTo(_parser(doc, true), longString, longString);
73+
}
74+
75+
/*
76+
/**********************************************************
77+
/* Test methods, generator round-trip
78+
/**********************************************************
79+
*/
80+
81+
// Round-trip through `CBORGenerator` with STRINGREF enabled. Needs to use
82+
// `SerializableString` (or `writeRawUTF8String()`): plain `writeString()`
83+
// chunks Strings this long, see `testLongChunkedStringNotReferenced()`
84+
@Test
85+
public void testLongStringRoundTrip() throws Exception
86+
{
87+
final String longString = _generateAscii(LONG_LENGTH);
88+
final SerializedString longSerialized = new SerializedString(longString);
89+
90+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
91+
try (CBORGenerator gen = stringrefCborGenerator(bytes)) {
92+
gen.writeStartArray();
93+
gen.writeString(longSerialized);
94+
gen.writeString("AAA");
95+
// Written as reference to entry #0:
96+
gen.writeString(longSerialized);
97+
// ... and this one as reference to entry #1:
98+
gen.writeString("AAA");
99+
gen.writeEndArray();
100+
}
101+
final byte[] doc = bytes.toByteArray();
102+
103+
for (boolean stream : new boolean[] { false, true }) {
104+
try (JsonParser p = _parser(doc, stream)) {
105+
assertToken(JsonToken.START_ARRAY, p.nextToken());
106+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
107+
assertEquals(longString, p.getText());
108+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
109+
assertEquals("AAA", p.getText());
110+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
111+
assertEquals(longString, p.getText(),
112+
"Reference to long String, stream = "+stream);
113+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
114+
assertEquals("AAA", p.getText(),
115+
"Reference to short String, stream = "+stream);
116+
assertToken(JsonToken.END_ARRAY, p.nextToken());
117+
assertNull(p.nextToken());
118+
}
119+
}
120+
}
121+
122+
// Conversely: chunked (indefinite length) Strings must NOT be added to the
123+
// reference table, by either side. Generator chunks Strings longer than
124+
// ~4000 characters when written via `writeString(String)`, so the long
125+
// String below does not take up an index -- and "AAA" is entry #0
126+
@Test
127+
public void testLongChunkedStringNotReferenced() throws Exception
128+
{
129+
final String longString = _generateAscii(LONG_LENGTH);
130+
131+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
132+
try (CBORGenerator gen = stringrefCborGenerator(bytes)) {
133+
gen.writeStartArray();
134+
gen.writeString(longString);
135+
gen.writeString("AAA");
136+
// Written as reference to entry #0, that is, "AAA":
137+
gen.writeString("AAA");
138+
gen.writeEndArray();
139+
}
140+
final byte[] doc = bytes.toByteArray();
141+
142+
for (boolean stream : new boolean[] { false, true }) {
143+
try (JsonParser p = _parser(doc, stream)) {
144+
assertToken(JsonToken.START_ARRAY, p.nextToken());
145+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
146+
assertEquals(longString, p.getText());
147+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
148+
assertEquals("AAA", p.getText());
149+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
150+
assertEquals("AAA", p.getText(),
151+
"Reference to short String, stream = "+stream);
152+
assertToken(JsonToken.END_ARRAY, p.nextToken());
153+
assertNull(p.nextToken());
154+
}
155+
}
156+
}
157+
158+
/*
159+
/**********************************************************
160+
/* Helper methods
161+
/**********************************************************
162+
*/
163+
164+
/**
165+
* Constructs document
166+
*<pre>
167+
* tag(256) [ &lt;longString&gt;, "AAA", "BBB", tag(25) refIndex ]
168+
*</pre>
169+
* where encoder-side reference table is {@code longString} = #0,
170+
* {@code "AAA"} = #1, {@code "BBB"} = #2.
171+
*/
172+
private byte[] _stringRefDoc(String longString, int refIndex) throws Exception
173+
{
174+
final byte[] rawString = utf8Bytes(longString);
175+
ByteArrayOutputStream b = new ByteArrayOutputStream();
176+
b.write(0xD9); b.write(0x01); b.write(0x00); // tag 256, "stringref-namespace"
177+
b.write(0x84); // Array, 4 elements
178+
// text, 4-byte length (works for any length we use here)
179+
b.write(0x7A);
180+
b.write(rawString.length >> 24);
181+
b.write((rawString.length >> 16) & 0xFF);
182+
b.write((rawString.length >> 8) & 0xFF);
183+
b.write(rawString.length & 0xFF);
184+
b.write(rawString, 0, rawString.length);
185+
b.write(0x63); b.write('A'); b.write('A'); b.write('A');
186+
b.write(0x63); b.write('B'); b.write('B'); b.write('B');
187+
b.write(0xD8); b.write(0x19); b.write(refIndex); // tag 25, "stringref"
188+
return b.toByteArray();
189+
}
190+
191+
// Reads through document produced by `_stringRefDoc()`, verifying that
192+
// the trailing reference resolves to expected String
193+
private void _verifyRefResolvesTo(JsonParser p, String longString, String exp)
194+
throws Exception
195+
{
196+
try {
197+
assertToken(JsonToken.START_ARRAY, p.nextToken());
198+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
199+
assertEquals(longString, p.getText());
200+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
201+
assertEquals("AAA", p.getText());
202+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
203+
assertEquals("BBB", p.getText());
204+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
205+
assertEquals(exp, p.getText());
206+
assertToken(JsonToken.END_ARRAY, p.nextToken());
207+
assertNull(p.nextToken());
208+
} finally {
209+
p.close();
210+
}
211+
}
212+
213+
// `byte[]`-backed parser decodes long Strings via `_finishShortText()` and
214+
// was never affected; `InputStream`-backed one uses `_finishLongText()`
215+
private JsonParser _parser(byte[] doc, boolean stream) throws Exception {
216+
return stream ? cborParser(new ByteArrayInputStream(doc)) : cborParser(doc);
217+
}
218+
219+
private String _generateAscii(int len) {
220+
StringBuilder sb = new StringBuilder(len);
221+
while (sb.length() < len) {
222+
sb.append((char) ('a' + (sb.length() % 26)));
223+
}
224+
return sb.toString();
225+
}
226+
}

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ No changes since 2.19.1
107107
marker 23 with 24 ("1-byte length suffix follows")
108108
#728: (cbor) `CBORParser.nextFieldName(SerializableString)` consumes Object entry
109109
slot twice on fast-path miss, truncating definite-length Objects
110+
#733: (cbor) Long `String`s not added to "stringref" reference table, breaking
111+
following references
110112

111113
2.18.9 (07-Jul-2026)
112114

0 commit comments

Comments
 (0)