diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fae6d21f5..5589cdc2e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - java_version: ['8', '11', '17', '21'] + java_version: ['8', '17', '21'] include: - java_version: '8' release_build: 'R' diff --git a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java index 7af94d27c..533db5e5b 100644 --- a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java +++ b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java @@ -2620,7 +2620,8 @@ private final String _finishLongText(int len) throws IOException } /** - * Consumes as many ascii chars as possible in a tight loop. Returns the amount of bytes remaining. + * Consumes as many ascii chars as possible in a tight loop. + * Returns the amount of bytes remaining. */ private final int _finishLongTextAscii(int len) throws IOException { @@ -2646,7 +2647,10 @@ private final int _finishLongTextAscii(int len) throws IOException --outPtr; _inputPtr = inPtr - 1; _textBuffer.setCurrentLength(outPtr); - return len - outPtr; + // `len` was already decremented for all previous iterations; subtract only + // the bytes consumed in THIS iteration (= _inputPtr), since + // _tryToLoadToHaveAtLeast always resets _inputPtr to 0 before the inner loop. + return len - _inputPtr; } _inputPtr = inPtr; if (outPtr >= outBuf.length) { diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/CBORLongAsciiRead686Test.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/CBORLongAsciiRead686Test.java new file mode 100644 index 000000000..f832f6ef8 --- /dev/null +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/CBORLongAsciiRead686Test.java @@ -0,0 +1,81 @@ +package com.fasterxml.jackson.dataformat.cbor.parse; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +// For [dataformats-binary#686] +public class CBORLongAsciiRead686Test +{ + // TextBuffer segment sizes grow 1.5× per flip, starting at 200 chars (MIN=500): + // S0=200, S1=500, S2=750, S3=1125, S4=1687, S5=2530, S6=3795, S7=5692, S8=8538 + // After 8 flips the segment is 8538 chars — the first one larger than the 8000-byte I/O buffer. + // Total chars consumed reaching S8: 200+500+750+1125+1687+2530+3795+5692 = 16279. + private static final int CHARS_TO_REACH_S8 = 16279; + private static final int IO_BUFFER_SIZE = 8000; + + /** + * Parses a definite-length CBOR map {a: <16279 'a' bytes>, b: <8000 'a' bytes + 0xB7>, n: "x"} + * via InputStream and expects all tokens to be read without error. + *
+ * Actual result on affected versions: JsonParseException "Unsupported major type (5)" + */ + @Test + public void testFinishLongTextAsciiDoesNotLeaveNonAsciiByte() + { + byte[] strA = new byte[CHARS_TO_REACH_S8]; + Arrays.fill(strA, (byte) 'a'); + + // 0xC2 0xB7 = U+00B7 "·" (middle dot): a valid 2-byte UTF-8 sequence. + // 0xC2 is non-ASCII so _finishLongTextAscii exits, but it must leave len non-negative + // so that _finishLongText's while loop can still decode the sequence. + byte[] strB = new byte[IO_BUFFER_SIZE + 2]; + Arrays.fill(strB, (byte) 'a'); + strB[IO_BUFFER_SIZE] = (byte) 0xC2; + strB[IO_BUFFER_SIZE + 1] = (byte) 0xB7; + + assertDoesNotThrow(() -> + new ObjectMapper(new CBORFactory()) + .readValue(new ByteArrayInputStream(buildMap(strA, strB)), Object.class)); + } + + /** + * definite-length map(3): {a: strA, b: strB, n: "x"} + */ + private static byte[] buildMap(byte[] strA, byte[] strB) throws IOException + { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(0xa3); + writeText(bos, new byte[]{'a'}); + writeText(bos, strA); + writeText(bos, new byte[]{'b'}); + writeText(bos, strB); + writeText(bos, new byte[]{'n'}); + writeText(bos, new byte[]{'x'}); + return bos.toByteArray(); + } + + private static void writeText(ByteArrayOutputStream bos, byte[] bytes) throws IOException + { + int n = bytes.length; + if (n <= 23) { + bos.write(0x60 | n); + } else if (n <= 0xFF) { + bos.write(0x78); + bos.write(n); + } else if (n <= 0xFFFF) { + bos.write(0x79); + bos.write((n >> 8) & 0xFF); + bos.write(n & 0xFF); + } + bos.write(bytes); + } +} diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index f2e46c421..453530011 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -429,5 +429,10 @@ Vincent Eigenberger (@beseder1) (2.20.1) Yohei Kishimoto (@morokosi) - * Reported #599: (cbor) Unable to deserialize stringref-enabled CBOR with ignored properties + * Reported #599: (cbor) Unable to deserialize stringref-enabled CBOR with ignored propertie (2.21.0) + +Halil İbrahim Şener (@hisener) + * Fixed #686: `CBORParser._finishLongTextAscii` returns negative length when `TextBuffer` + segment > I/O buffer, leaving non-ASCII byte unconsumed + (2.21.3) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 153c5e080..2e46031d0 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -14,6 +14,12 @@ Active maintainers: === Releases === ------------------------------------------------------------------------ +2.21.3 (not yet released) + +#686: `CBORParser._finishLongTextAscii` returns negative length when `TextBuffer` + segment > I/O buffer, leaving non-ASCII byte unconsumed + (fixed by Halil İbrahim Ş) + 2.21.2 (20-Mar-2026) No changes since 2.21.1.