Skip to content

Commit b1c1102

Browse files
authored
Fix _finishLongTextAscii returning negative remaining length (#687)
1 parent 5abfba3 commit b1c1102

5 files changed

Lines changed: 100 additions & 4 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
fail-fast: false
2020
matrix:
21-
java_version: ['8', '11', '17', '21']
21+
java_version: ['8', '17', '21']
2222
include:
2323
- java_version: '8'
2424
release_build: 'R'

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2620,7 +2620,8 @@ private final String _finishLongText(int len) throws IOException
26202620
}
26212621

26222622
/**
2623-
* Consumes as many ascii chars as possible in a tight loop. Returns the amount of bytes remaining.
2623+
* Consumes as many ascii chars as possible in a tight loop.
2624+
* Returns the amount of bytes remaining.
26242625
*/
26252626
private final int _finishLongTextAscii(int len) throws IOException
26262627
{
@@ -2646,7 +2647,10 @@ private final int _finishLongTextAscii(int len) throws IOException
26462647
--outPtr;
26472648
_inputPtr = inPtr - 1;
26482649
_textBuffer.setCurrentLength(outPtr);
2649-
return len - outPtr;
2650+
// `len` was already decremented for all previous iterations; subtract only
2651+
// the bytes consumed in THIS iteration (= _inputPtr), since
2652+
// _tryToLoadToHaveAtLeast always resets _inputPtr to 0 before the inner loop.
2653+
return len - _inputPtr;
26502654
}
26512655
_inputPtr = inPtr;
26522656
if (outPtr >= outBuf.length) {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.fasterxml.jackson.dataformat.cbor.parse;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.ByteArrayOutputStream;
5+
import java.io.IOException;
6+
import java.util.Arrays;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
12+
13+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
14+
15+
// For [dataformats-binary#686]
16+
public class CBORLongAsciiRead686Test
17+
{
18+
// TextBuffer segment sizes grow 1.5× per flip, starting at 200 chars (MIN=500):
19+
// S0=200, S1=500, S2=750, S3=1125, S4=1687, S5=2530, S6=3795, S7=5692, S8=8538
20+
// After 8 flips the segment is 8538 chars — the first one larger than the 8000-byte I/O buffer.
21+
// Total chars consumed reaching S8: 200+500+750+1125+1687+2530+3795+5692 = 16279.
22+
private static final int CHARS_TO_REACH_S8 = 16279;
23+
private static final int IO_BUFFER_SIZE = 8000;
24+
25+
/**
26+
* Parses a definite-length CBOR map {a: <16279 'a' bytes>, b: <8000 'a' bytes + 0xB7>, n: "x"}
27+
* via InputStream and expects all tokens to be read without error.
28+
* <p>
29+
* Actual result on affected versions: JsonParseException "Unsupported major type (5)"
30+
*/
31+
@Test
32+
public void testFinishLongTextAsciiDoesNotLeaveNonAsciiByte()
33+
{
34+
byte[] strA = new byte[CHARS_TO_REACH_S8];
35+
Arrays.fill(strA, (byte) 'a');
36+
37+
// 0xC2 0xB7 = U+00B7 "·" (middle dot): a valid 2-byte UTF-8 sequence.
38+
// 0xC2 is non-ASCII so _finishLongTextAscii exits, but it must leave len non-negative
39+
// so that _finishLongText's while loop can still decode the sequence.
40+
byte[] strB = new byte[IO_BUFFER_SIZE + 2];
41+
Arrays.fill(strB, (byte) 'a');
42+
strB[IO_BUFFER_SIZE] = (byte) 0xC2;
43+
strB[IO_BUFFER_SIZE + 1] = (byte) 0xB7;
44+
45+
assertDoesNotThrow(() ->
46+
new ObjectMapper(new CBORFactory())
47+
.readValue(new ByteArrayInputStream(buildMap(strA, strB)), Object.class));
48+
}
49+
50+
/**
51+
* definite-length map(3): {a: strA, b: strB, n: "x"}
52+
*/
53+
private static byte[] buildMap(byte[] strA, byte[] strB) throws IOException
54+
{
55+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
56+
bos.write(0xa3);
57+
writeText(bos, new byte[]{'a'});
58+
writeText(bos, strA);
59+
writeText(bos, new byte[]{'b'});
60+
writeText(bos, strB);
61+
writeText(bos, new byte[]{'n'});
62+
writeText(bos, new byte[]{'x'});
63+
return bos.toByteArray();
64+
}
65+
66+
private static void writeText(ByteArrayOutputStream bos, byte[] bytes) throws IOException
67+
{
68+
int n = bytes.length;
69+
if (n <= 23) {
70+
bos.write(0x60 | n);
71+
} else if (n <= 0xFF) {
72+
bos.write(0x78);
73+
bos.write(n);
74+
} else if (n <= 0xFFFF) {
75+
bos.write(0x79);
76+
bos.write((n >> 8) & 0xFF);
77+
bos.write(n & 0xFF);
78+
}
79+
bos.write(bytes);
80+
}
81+
}

release-notes/CREDITS-2.x

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,5 +429,10 @@ Vincent Eigenberger (@beseder1)
429429
(2.20.1)
430430

431431
Yohei Kishimoto (@morokosi)
432-
* Reported #599: (cbor) Unable to deserialize stringref-enabled CBOR with ignored properties
432+
* Reported #599: (cbor) Unable to deserialize stringref-enabled CBOR with ignored propertie
433433
(2.21.0)
434+
435+
Halil İbrahim Şener (@hisener)
436+
* Fixed #686: `CBORParser._finishLongTextAscii` returns negative length when `TextBuffer`
437+
segment > I/O buffer, leaving non-ASCII byte unconsumed
438+
(2.21.3)

release-notes/VERSION-2.x

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ Active maintainers:
1414
=== Releases ===
1515
------------------------------------------------------------------------
1616

17+
2.21.3 (not yet released)
18+
19+
#686: `CBORParser._finishLongTextAscii` returns negative length when `TextBuffer`
20+
segment > I/O buffer, leaving non-ASCII byte unconsumed
21+
(fixed by Halil İbrahim Ş)
22+
1723
2.21.2 (20-Mar-2026)
1824

1925
No changes since 2.21.1.

0 commit comments

Comments
 (0)