Skip to content

Commit 6192fc4

Browse files
committed
Merge branch '2.19' into 2.20
2 parents 73dc17c + 294590e commit 6192fc4

6 files changed

Lines changed: 264 additions & 36 deletions

File tree

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

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,7 +1613,13 @@ public String nextFieldName() throws IOException
16131613
final int lenMarker = ch & 0x1F;
16141614
_sharedString = null;
16151615
String name;
1616-
boolean chunked = false;
1616+
// 24-Jul-2026, tatu: [dataformats-binary#735] Actual byte length of the name,
1617+
// needed for "stringref" decision: the 5-bit marker is the length itself
1618+
// only for values 0 - 23. Stays negative for chunked (indefinite length)
1619+
// names: no separate check needed for those since every case of
1620+
// `shouldReferenceString()` requires a minimum length, so a negative
1621+
// length can never be referenced
1622+
int nameLen = lenMarker;
16171623
if (lenMarker <= 23) {
16181624
// NOTE: [dataformats-binary#725] `maxNameLength` NOT enforced for
16191625
// these shortest (at most 23 bytes) names; see
@@ -1637,19 +1643,18 @@ public String nextFieldName() throws IOException
16371643
}
16381644
}
16391645
} else {
1640-
final int actualLen = _decodeExplicitLength(lenMarker);
1641-
if (actualLen < 0) {
1642-
chunked = true;
1646+
nameLen = _decodeExplicitLength(lenMarker);
1647+
if (nameLen < 0) {
16431648
name = _decodeChunkedName();
16441649
} else {
16451650
// 24-Jul-2026, tatu: [dataformats-binary#725] Validate before
16461651
// decoding (or even reading) content
1647-
_streamReadConstraints.validateNameLength(actualLen);
1648-
name = _decodeLongerName(actualLen);
1652+
_streamReadConstraints.validateNameLength(nameLen);
1653+
name = _decodeLongerName(nameLen);
16491654
}
16501655
}
1651-
if (!chunked && !_stringRefs.empty() &&
1652-
shouldReferenceString(_stringRefs.peek().stringRefs.size(), lenMarker)) {
1656+
if (!_stringRefs.empty() &&
1657+
shouldReferenceString(_stringRefs.peek().stringRefs.size(), nameLen)) {
16531658
_stringRefs.peek().stringRefs.add(name);
16541659
_sharedString = name;
16551660
}
@@ -3118,8 +3123,14 @@ protected final JsonToken _decodePropertyName() throws IOException
31183123
return JsonToken.FIELD_NAME;
31193124
}
31203125
final int lenMarker = ch & 0x1F;
3121-
boolean chunked = false;
31223126
String name;
3127+
// 24-Jul-2026, tatu: [dataformats-binary#735] Actual byte length of the name,
3128+
// needed for "stringref" decision: the 5-bit marker is the length itself
3129+
// only for values 0 - 23. Stays negative for chunked (indefinite length)
3130+
// names: no separate check needed for those since every case of
3131+
// `shouldReferenceString()` requires a minimum length, so a negative
3132+
// length can never be referenced
3133+
int nameLen = lenMarker;
31233134
if (lenMarker <= 23) {
31243135
// NOTE: [dataformats-binary#725] `maxNameLength` NOT enforced for
31253136
// these shortest (at most 23 bytes) names: enforcement is
@@ -3144,19 +3155,18 @@ protected final JsonToken _decodePropertyName() throws IOException
31443155
}
31453156
}
31463157
} else {
3147-
final int actualLen = _decodeExplicitLength(lenMarker);
3148-
if (actualLen < 0) {
3149-
chunked = true;
3158+
nameLen = _decodeExplicitLength(lenMarker);
3159+
if (nameLen < 0) {
31503160
name = _decodeChunkedName();
31513161
} else {
31523162
// 24-Jul-2026, tatu: [dataformats-binary#725] Validate before
31533163
// decoding (or even reading) content
3154-
_streamReadConstraints.validateNameLength(actualLen);
3155-
name = _decodeLongerName(actualLen);
3164+
_streamReadConstraints.validateNameLength(nameLen);
3165+
name = _decodeLongerName(nameLen);
31563166
}
31573167
}
3158-
if (!chunked && !_stringRefs.empty() &&
3159-
shouldReferenceString(_stringRefs.peek().stringRefs.size(), lenMarker)) {
3168+
if (!_stringRefs.empty() &&
3169+
shouldReferenceString(_stringRefs.peek().stringRefs.size(), nameLen)) {
31603170
_stringRefs.peek().stringRefs.add(name);
31613171
_sharedString = name;
31623172
}

cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,20 @@ protected String getAndVerifyText(JsonParser p) throws IOException
251251
/**********************************************************
252252
*/
253253

254+
/**
255+
* Generates a String of exactly {@code length} ASCII characters (and, since
256+
* they are ASCII, exactly {@code length} bytes when UTF-8 encoded). Unlike
257+
* {@link #generateLongAsciiString} which pads in word-sized chunks and may
258+
* overshoot, this is for tests that need a precise encoded length.
259+
*/
260+
protected static String generateAsciiString(int length) {
261+
StringBuilder sb = new StringBuilder(length);
262+
while (sb.length() < length) {
263+
sb.append((char) ('a' + (sb.length() % 26)));
264+
}
265+
return sb.toString();
266+
}
267+
254268
protected static String generateUnicodeString(int length) {
255269
return generateUnicodeString(length, new Random(length));
256270
}

cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/GeneratorShortStringTest.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,6 @@ public void testMediumTextAsCharArray() throws Exception {
9797
}
9898
}
9999

100-
private String generateAsciiString(int len) {
101-
StringBuilder sb = new StringBuilder(len);
102-
while (--len >= 0) {
103-
sb.append((char) ('0' + (len % 10)));
104-
}
105-
return sb.toString();
106-
}
107-
108100
private void _verifyString(byte[] encoded, String value) throws Exception
109101
{
110102
try (JsonParser p = cborParser(encoded)) {

cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/StringRef733Test.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class StringRef733Test extends CBORTestBase
3737
@Test
3838
public void testLongStringDoesNotShiftFollowingRefs() throws Exception
3939
{
40-
final String longString = _generateAscii(LONG_LENGTH);
40+
final String longString = generateAsciiString(LONG_LENGTH);
4141
final byte[] doc = _stringRefDoc(longString, 1);
4242

4343
_verifyRefResolvesTo(_parser(doc, false), longString, "AAA");
@@ -49,7 +49,7 @@ public void testLongStringDoesNotShiftFollowingRefs() throws Exception
4949
@Test
5050
public void testReferenceToLongStringItself() throws Exception
5151
{
52-
final String longString = _generateAscii(LONG_LENGTH);
52+
final String longString = generateAsciiString(LONG_LENGTH);
5353
final byte[] doc = _stringRefDoc(longString, 0);
5454

5555
_verifyRefResolvesTo(_parser(doc, false), longString, longString);
@@ -84,7 +84,7 @@ public void testReferenceToLongUnicodeString() throws Exception
8484
@Test
8585
public void testLongStringRoundTrip() throws Exception
8686
{
87-
final String longString = _generateAscii(LONG_LENGTH);
87+
final String longString = generateAsciiString(LONG_LENGTH);
8888
final SerializedString longSerialized = new SerializedString(longString);
8989

9090
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
@@ -126,7 +126,7 @@ public void testLongStringRoundTrip() throws Exception
126126
@Test
127127
public void testLongChunkedStringNotReferenced() throws Exception
128128
{
129-
final String longString = _generateAscii(LONG_LENGTH);
129+
final String longString = generateAsciiString(LONG_LENGTH);
130130

131131
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
132132
try (CBORGenerator gen = stringrefCborGenerator(bytes)) {
@@ -215,12 +215,4 @@ private void _verifyRefResolvesTo(JsonParser p, String longString, String exp)
215215
private JsonParser _parser(byte[] doc, boolean stream) throws Exception {
216216
return stream ? cborParser(new ByteArrayInputStream(doc)) : cborParser(doc);
217217
}
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-
}
226218
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package com.fasterxml.jackson.dataformat.cbor;
2+
3+
import java.io.ByteArrayOutputStream;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import com.fasterxml.jackson.core.JsonParser;
8+
import com.fasterxml.jackson.core.JsonToken;
9+
import com.fasterxml.jackson.core.io.SerializedString;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
/**
14+
* Tests for [dataformats-binary#735]: the Object property name paths used to pass
15+
* the 5-bit length marker, instead of the name's actual byte length, to
16+
* {@code shouldReferenceString()}. Markers 24 - 27 ("1/2/4/8-byte length suffix
17+
* follows") are all above every minimum-length threshold, so a name shorter than
18+
* the threshold got registered in the "stringref" table when a conformant encoder
19+
* would have skipped it -- shifting all following reference indexes by one.
20+
*<p>
21+
* Only reachable for non-canonically encoded length prefixes: canonical marker 24
22+
* implies length &gt;= 24, marker 25 length &gt;= 256, and so on, all above the
23+
* thresholds. Jackson's own generator always writes minimal prefixes, so these
24+
* documents are hand-crafted.
25+
*/
26+
public class StringRef735Test extends CBORTestBase
27+
{
28+
// How the name is to be read. Modes 1 and 2 exercise the two paths that
29+
// passed `lenMarker`; mode 3's fast path always used the real byte length,
30+
// and is included to keep it that way (it only reaches the shared paths for
31+
// markers above 24, which it does not handle itself)
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+
// 2-byte name written with a 1-byte length suffix (marker 24): below the
43+
// 3-byte minimum for index #0, so a conformant encoder does NOT give it an
44+
// index -- making "AAA" entry #0, and reference #0 resolve to "AAA"
45+
@Test
46+
public void testShortNameWithLongMarkerNotReferenced() throws Exception
47+
{
48+
final String name = "ab";
49+
final byte[] doc = _doc(name, _nonCanonical1ByteLen(name));
50+
51+
_verifyAllModes(doc, name, "AAA");
52+
}
53+
54+
// Same, but with a 2-byte length suffix (marker 25) for a 2-byte name
55+
@Test
56+
public void testShortNameWith2ByteMarkerNotReferenced() throws Exception
57+
{
58+
final String name = "ab";
59+
final byte[] doc = _doc(name, _nonCanonical2ByteLen(name));
60+
61+
_verifyAllModes(doc, name, "AAA");
62+
}
63+
64+
// Conversely: a name that IS long enough must still be registered. Here the
65+
// name is entry #0 and "AAA" entry #1, so reference #0 is the name itself --
66+
// verifies the fix did not simply stop registering names
67+
@Test
68+
public void testLongEnoughNameStillReferenced() throws Exception
69+
{
70+
final String name = generateAsciiString(30); // canonical marker 24
71+
final byte[] doc = _doc(name, _nonCanonical1ByteLen(name));
72+
73+
_verifyAllModes(doc, name, name);
74+
}
75+
76+
// Exactly at the 3-byte threshold for index #0: registered either way, but
77+
// worth pinning since it is the boundary the marker value used to mask
78+
@Test
79+
public void testNameAtThresholdReferenced() throws Exception
80+
{
81+
final String name = "abc";
82+
final byte[] doc = _doc(name, _nonCanonical1ByteLen(name));
83+
84+
_verifyAllModes(doc, name, name);
85+
}
86+
87+
// Chunked (indefinite length) names are never referenced, no matter how long
88+
// they are: the decoded length is not known when the marker is read, and a
89+
// conformant encoder only indexes definite-length strings. So "AAA" is entry
90+
// #0 here even though the name is 4 bytes
91+
@Test
92+
public void testChunkedNameNotReferenced() throws Exception
93+
{
94+
final byte[] doc = _chunkedNameDoc("ab", "cd");
95+
96+
_verifyAllModes(doc, "abcd", "AAA");
97+
}
98+
99+
/*
100+
/**********************************************************
101+
/* Helper methods, document construction
102+
/**********************************************************
103+
*/
104+
105+
/**
106+
* Builds document
107+
*<pre>
108+
* tag(256) [ { &lt;name&gt; : "AAA" }, tag(25) 0 ]
109+
*</pre>
110+
* with the property name encoded using the given (possibly non-canonical)
111+
* length prefix. Reference is always to entry #0, which is what makes the
112+
* tests sensitive to whether the name took up an index: if it did, #0 is the
113+
* name, and if it did not, #0 is the {@code "AAA"} value.
114+
*/
115+
private byte[] _doc(String name, byte[] namePrefix) throws Exception
116+
{
117+
final byte[] rawName = utf8Bytes(name);
118+
ByteArrayOutputStream b = new ByteArrayOutputStream();
119+
b.write(0xD9); b.write(0x01); b.write(0x00); // tag 256, "stringref-namespace"
120+
b.write(0x82); // Array, 2 elements
121+
b.write(0xA1); // Object, 1 entry
122+
b.write(namePrefix, 0, namePrefix.length);
123+
b.write(rawName, 0, rawName.length);
124+
b.write(0x63); b.write('A'); b.write('A'); b.write('A');
125+
b.write(0xD8); b.write(0x19); b.write(0x00); // tag 25, "stringref" to entry #0
126+
return b.toByteArray();
127+
}
128+
129+
/**
130+
* Same shape as {@link #_doc}, but with the property name written as chunked
131+
* (indefinite length) text made up of the given chunks.
132+
*/
133+
private byte[] _chunkedNameDoc(String... chunks) throws Exception
134+
{
135+
ByteArrayOutputStream b = new ByteArrayOutputStream();
136+
b.write(0xD9); b.write(0x01); b.write(0x00); // tag 256, "stringref-namespace"
137+
b.write(0x82); // Array, 2 elements
138+
b.write(0xA1); // Object, 1 entry
139+
b.write(0x7F); // text, indefinite length
140+
for (String chunk : chunks) {
141+
final byte[] raw = utf8Bytes(chunk);
142+
b.write(0x60 + raw.length); // text, length in type byte
143+
b.write(raw, 0, raw.length);
144+
}
145+
b.write(0xFF); // break
146+
b.write(0x63); b.write('A'); b.write('A'); b.write('A');
147+
b.write(0xD8); b.write(0x19); b.write(0x00); // tag 25, "stringref" to entry #0
148+
return b.toByteArray();
149+
}
150+
151+
// Length prefix using marker 24, "1-byte length suffix follows"
152+
private byte[] _nonCanonical1ByteLen(String name) {
153+
final int len = utf8Bytes(name).length;
154+
return new byte[] { (byte) 0x78, (byte) len };
155+
}
156+
157+
// Length prefix using marker 25, "2-byte length suffix follows"
158+
private byte[] _nonCanonical2ByteLen(String name) {
159+
final int len = utf8Bytes(name).length;
160+
return new byte[] { (byte) 0x79, (byte) (len >> 8), (byte) len };
161+
}
162+
163+
/*
164+
/**********************************************************
165+
/* Helper methods, verification
166+
/**********************************************************
167+
*/
168+
169+
// Runs every read mode, reporting all failures: the two name-decoding paths
170+
// (`_decodePropertyName()` and the one inlined in `nextFieldName()`) had
171+
// separate copies of the faulty check, so each needs its own coverage
172+
private void _verifyAllModes(byte[] doc, String expName, String expRef)
173+
{
174+
assertAll(
175+
() -> _verifyRef(doc, expName, expRef, MODE_NEXT_TOKEN),
176+
() -> _verifyRef(doc, expName, expRef, MODE_NEXT_FIELD_NAME),
177+
() -> _verifyRef(doc, expName, expRef, MODE_NEXT_FIELD_NAME_MATCH));
178+
}
179+
180+
private void _verifyRef(byte[] doc, String expName, String expRef, int mode)
181+
throws Exception
182+
{
183+
final String desc = "(mode: "+mode+")";
184+
try (JsonParser p = cborParser(doc)) {
185+
assertToken(JsonToken.START_ARRAY, p.nextToken());
186+
assertToken(JsonToken.START_OBJECT, p.nextToken());
187+
_advanceToName(p, expName, mode);
188+
assertToken(JsonToken.FIELD_NAME, p.currentToken());
189+
assertEquals(expName, p.currentName(), desc);
190+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
191+
assertEquals("AAA", p.getText(), desc);
192+
assertToken(JsonToken.END_OBJECT, p.nextToken());
193+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
194+
assertEquals(expRef, p.getText(), desc);
195+
assertToken(JsonToken.END_ARRAY, p.nextToken());
196+
assertNull(p.nextToken());
197+
}
198+
}
199+
200+
private void _advanceToName(JsonParser p, String expName, int mode)
201+
throws Exception
202+
{
203+
switch (mode) {
204+
case MODE_NEXT_TOKEN:
205+
assertToken(JsonToken.FIELD_NAME, p.nextToken());
206+
break;
207+
case MODE_NEXT_FIELD_NAME:
208+
assertEquals(expName, p.nextFieldName());
209+
break;
210+
case MODE_NEXT_FIELD_NAME_MATCH:
211+
assertTrue(p.nextFieldName(new SerializedString(expName)),
212+
"Should match name '"+expName+"'");
213+
break;
214+
default:
215+
fail("Unknown mode: "+mode);
216+
}
217+
}
218+
}

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ No changes since 2.19.1
109109
slot twice on fast-path miss, truncating definite-length Objects
110110
#733: (cbor) Long `String`s not added to "stringref" reference table, breaking
111111
following references
112+
#735: (cbor) "stringref" property-name paths pass 5-bit length marker instead of
113+
actual length to `shouldReferenceString()`
112114

113115
2.18.9 (07-Jul-2026)
114116

0 commit comments

Comments
 (0)