From dafbbcfe658dd21c7f52d91b690c060cb838ce89 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 13:40:57 -0700 Subject: [PATCH 1/4] Fix #728 --- .../jackson/dataformat/cbor/CBORParser.java | 10 + .../cbor/parse/NextFieldName728Test.java | 182 ++++++++++++++++++ release-notes/VERSION-2.x | 2 + 3 files changed, 194 insertions(+) create mode 100644 cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java 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 fdee657fc..2f2f43e8c 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 @@ -1454,6 +1454,16 @@ public boolean nextFieldName(SerializableString str) throws IOException } } } + // 24-Jul-2026, tatu: [dataformats-binary#728] Cannot fall back to + // `nextToken()` here: we have already consumed one entry of + // expected-length Object (`expectMoreValues()` above) and + // `nextToken()` would consume another one. So decode name here. + final JsonToken t = _decodePropertyName(); + if (t == null) { // end-of-input; only for root context, else fails + return false; + } + return (_updateToken(t) == JsonToken.FIELD_NAME) + && str.getValue().equals(getCurrentName()); } // otherwise just fall back to default handling; should occur rarely return (nextToken() == JsonToken.FIELD_NAME) && str.getValue().equals(getCurrentName()); diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java new file mode 100644 index 000000000..d7f5355e0 --- /dev/null +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java @@ -0,0 +1,182 @@ +package com.fasterxml.jackson.dataformat.cbor.parse; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.io.SerializedString; + +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import com.fasterxml.jackson.dataformat.cbor.CBORTestBase; +import com.fasterxml.jackson.dataformat.cbor.testutil.ThrottledInputStream; + +// [dataformats-binary#728]: `nextFieldName(SerializableString)` used to consume +// an entry of expected-length (definite-length) Object twice, when its +// name-matching fast path did not succeed, truncating the Object +public class NextFieldName728Test extends CBORTestBase +{ + private final CBORFactory F = new CBORFactory(); + + // {"a":1, "b":2} as definite-length Object; name not matched + public void testNonMatchingNameDefiniteLength() throws Exception + { + final byte[] DOC = twoEntryObject(false); + + for (boolean stream : new boolean[] { false, true }) { + try (JsonParser p = _parser(DOC, stream)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertToken(JsonToken.FIELD_NAME, p.currentToken()); + assertEquals("a", p.currentName()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(1, p.getIntValue()); + // and second entry must NOT be lost: + assertToken(JsonToken.FIELD_NAME, p.nextToken()); + assertEquals("b", p.currentName()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(2, p.getIntValue()); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + } + } + } + + // Same, but name of second entry is the one not matched + public void testNonMatchingSecondNameDefiniteLength() throws Exception + { + final byte[] DOC = twoEntryObject(false); + + for (boolean stream : new boolean[] { false, true }) { + try (JsonParser p = _parser(DOC, stream)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertTrue(p.nextFieldName(new SerializedString("a"))); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(1, p.getIntValue()); + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertToken(JsonToken.FIELD_NAME, p.currentToken()); + assertEquals("b", p.currentName()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(2, p.getIntValue()); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + } + } + } + + // Matching name, but read from a stream that only has partial content + // buffered, so that the fast path cannot be used either + public void testMatchingNameDefiniteLengthThrottled() throws Exception + { + final byte[] DOC = twoEntryObject(false); + + try (JsonParser p = _parser(DOC, true)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertTrue(p.nextFieldName(new SerializedString("a"))); + assertEquals(1, p.nextIntValue(-1)); + assertTrue(p.nextFieldName(new SerializedString("b"))); + assertEquals(2, p.nextIntValue(-1)); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + } + } + + // Single-entry definite-length Object: used to report END_OBJECT (and + // `null` name) instead of the entry it does contain + public void testNonMatchingNameSingleEntry() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(0xA1); // Object, 1 entry + bytes.write(0x61); bytes.write('a'); + bytes.write(0x01); + final byte[] DOC = bytes.toByteArray(); + + for (boolean stream : new boolean[] { false, true }) { + try (JsonParser p = _parser(DOC, stream)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertToken(JsonToken.FIELD_NAME, p.currentToken()); + assertEquals("a", p.currentName()); + assertEquals(1, p.nextIntValue(-1)); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + } + } + } + + // Nested definite-length Objects, to verify context handling + public void testNonMatchingNameNested() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(0xA2); // Object, 2 entries + bytes.write(0x61); bytes.write('a'); + bytes.write(0xA1); // Object, 1 entry + bytes.write(0x61); bytes.write('c'); + bytes.write(0x03); + bytes.write(0x61); bytes.write('b'); + bytes.write(0x02); + final byte[] DOC = bytes.toByteArray(); + + for (boolean stream : new boolean[] { false, true }) { + try (JsonParser p = _parser(DOC, stream)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertEquals("a", p.currentName()); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertEquals("c", p.currentName()); + assertEquals(3, p.nextIntValue(-1)); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertToken(JsonToken.FIELD_NAME, p.nextToken()); + assertEquals("b", p.currentName()); + assertEquals(2, p.nextIntValue(-1)); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + } + } + } + + // Indefinite-length Object (what `CBORGenerator` writes): was, and + // stays, unaffected + public void testNonMatchingNameIndefiniteLength() throws Exception + { + final byte[] DOC = twoEntryObject(true); + + for (boolean stream : new boolean[] { false, true }) { + try (JsonParser p = _parser(DOC, stream)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertToken(JsonToken.FIELD_NAME, p.currentToken()); + assertEquals("a", p.currentName()); + assertEquals(1, p.nextIntValue(-1)); + assertToken(JsonToken.FIELD_NAME, p.nextToken()); + assertEquals("b", p.currentName()); + assertEquals(2, p.nextIntValue(-1)); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + } + } + } + + // {"a":1, "b":2}, either as definite- or indefinite-length Object + private byte[] twoEntryObject(boolean indefinite) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(indefinite ? 0xBF : 0xA2); + bytes.write(0x61); bytes.write('a'); + bytes.write(0x01); + bytes.write(0x61); bytes.write('b'); + bytes.write(0x02); + if (indefinite) { + bytes.write(0xFF); + } + return bytes.toByteArray(); + } + + private JsonParser _parser(byte[] doc, boolean stream) throws Exception { + if (!stream) { + return F.createParser(doc); + } + // read one byte at a time, so fast path cannot peek at name + return F.createParser(new ThrottledInputStream(new ByteArrayInputStream(doc), 1)); + } +} diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index fbeddaf70..663eae388 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -18,6 +18,8 @@ Active maintainers: #727: (cbor) `CBORParser.nextFieldName(SerializableString)` confuses 5-bit length marker 23 with 24 ("1-byte length suffix follows") +#728: (cbor) `CBORParser.nextFieldName(SerializableString)` consumes Object entry + slot twice on fast-path miss, truncating definite-length Objects 2.18.9 (07-Jul-2026) From 2874b50d4996be284bf968f11bcc9f66a2461309 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 13:51:48 -0700 Subject: [PATCH 2/4] Clean up --- .../jackson/dataformat/cbor/CBORParser.java | 4 +- .../jackson/dataformat/cbor/CBORTestBase.java | 15 +++ .../cbor/parse/NextFieldName727Test.java | 35 ++--- .../cbor/parse/NextFieldName728Test.java | 122 ++++++++++++++---- 4 files changed, 128 insertions(+), 48 deletions(-) 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 2f2f43e8c..686a0cad8 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 @@ -1459,7 +1459,9 @@ public boolean nextFieldName(SerializableString str) throws IOException // expected-length Object (`expectMoreValues()` above) and // `nextToken()` would consume another one. So decode name here. final JsonToken t = _decodePropertyName(); - if (t == null) { // end-of-input; only for root context, else fails + // Note: should not get `null` (for end-of-input) since we are within + // Object and `_handleEOF()` fails for that case; but just in case: + if (t == null) { return false; } return (_updateToken(t) == JsonToken.FIELD_NAME) diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java index 185e5cd49..19c62ede2 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper; +import com.fasterxml.jackson.dataformat.cbor.testutil.ThrottledInputStream; public abstract class CBORTestBase extends junit.framework.TestCase @@ -42,6 +43,20 @@ protected CBORParser cborParser(CBORFactory f, byte[] input) throws IOException return f.createParser(input); } + /** + * Helper for cases that need to verify handling both with fully buffered + * input (fixed {@code byte[]}) and with input that trickles in, one byte + * at a time, so that parser cannot rely on look-ahead within buffer. + * + * @since 2.18.10 + */ + protected CBORParser cborParser(byte[] input, boolean throttled) throws IOException { + if (throttled) { + return cborParser(new ThrottledInputStream(input, 1)); + } + return cborParser(input); + } + protected CBORParser cborParser(CBORFactory f, InputStream in) throws IOException { return f.createParser(in); } diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java index e6f39d881..c1ecdf705 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java @@ -1,6 +1,5 @@ package com.fasterxml.jackson.dataformat.cbor.parse; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import com.fasterxml.jackson.core.JsonGenerator; @@ -9,16 +8,12 @@ import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.SerializedString; -import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.cbor.CBORTestBase; -import com.fasterxml.jackson.dataformat.cbor.testutil.ThrottledInputStream; // [dataformats-binary#727]: `nextFieldName(SerializableString)` used to confuse // 5-bit length marker 23 (length as-is) with 24 ("1-byte length suffix follows") public class NextFieldName727Test extends CBORTestBase { - private final CBORFactory F = new CBORFactory(); - // Name of exactly 23 bytes, starting with control character U+0017 (0x17): // the mis-decoded length marker used to consume that first name byte as // the length, which made a 23-character name of a different value match @@ -39,8 +34,8 @@ public void testNoFalseMatchForLength23Name() throws Exception final String actualName = "\u0017" + repeat('a', 22); final SerializableString differentName = new SerializedString(repeat('a', 23)); - for (boolean stream : new boolean[] { false, true }) { - try (JsonParser p = _parser(DOC, stream)) { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(DOC, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertFalse(p.nextFieldName(differentName)); assertToken(JsonToken.FIELD_NAME, p.currentToken()); @@ -52,7 +47,7 @@ public void testNoFalseMatchForLength23Name() throws Exception } // ... and the actual name of course still matches - try (JsonParser p = _parser(DOC, stream)) { + try (JsonParser p = cborParser(DOC, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertTrue(p.nextFieldName(new SerializedString(actualName))); assertToken(JsonToken.FIELD_NAME, p.currentToken()); @@ -73,26 +68,26 @@ public void testNameLengths() throws Exception final String differentName = repeat('a', len - 1) + 'c'; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - try (JsonGenerator g = F.createGenerator(bytes)) { + try (JsonGenerator g = cborGenerator(bytes)) { g.writeStartObject(); g.writeStringField(name, "v"); g.writeEndObject(); } final byte[] doc = bytes.toByteArray(); - for (boolean stream : new boolean[] { false, true }) { - try (JsonParser p = _parser(doc, stream)) { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(doc, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); - assertTrue("Should match name of length "+len+" (stream? "+stream+")", + assertTrue("Should match name of length "+len+" (throttled? "+throttled+")", p.nextFieldName(new SerializedString(name))); assertEquals(name, p.currentName()); assertEquals("v", p.nextTextValue()); assertToken(JsonToken.END_OBJECT, p.nextToken()); assertNull(p.nextToken()); } - try (JsonParser p = _parser(doc, stream)) { + try (JsonParser p = cborParser(doc, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); - assertFalse("Should not match different name of length "+len+" (stream? "+stream+")", + assertFalse("Should not match different name of length "+len+" (throttled? "+throttled+")", p.nextFieldName(new SerializedString(differentName))); assertToken(JsonToken.FIELD_NAME, p.currentToken()); assertEquals(name, p.currentName()); @@ -120,7 +115,7 @@ public void testNonCanonicalLengthEncoding() throws Exception bytes.write(0xFF); // end indefinite-length Object final byte[] DOC = bytes.toByteArray(); - try (JsonParser p = _parser(DOC, false)) { + try (JsonParser p = cborParser(DOC)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertTrue(p.nextFieldName(new SerializedString(name))); assertEquals(name, p.currentName()); @@ -129,7 +124,7 @@ public void testNonCanonicalLengthEncoding() throws Exception assertNull(p.nextToken()); } - try (JsonParser p = _parser(DOC, false)) { + try (JsonParser p = cborParser(DOC)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertFalse(p.nextFieldName(new SerializedString("abcdX"))); assertToken(JsonToken.FIELD_NAME, p.currentToken()); @@ -137,14 +132,6 @@ public void testNonCanonicalLengthEncoding() throws Exception } } - private JsonParser _parser(byte[] doc, boolean stream) throws Exception { - if (!stream) { - return F.createParser(doc); - } - // read one byte at a time, to force the "not enough buffered" fallback - return F.createParser(new ThrottledInputStream(new ByteArrayInputStream(doc), 1)); - } - private String repeat(char c, int count) { StringBuilder sb = new StringBuilder(count); for (int i = 0; i < count; ++i) { diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java index d7f5355e0..a74264506 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java @@ -1,30 +1,25 @@ package com.fasterxml.jackson.dataformat.cbor.parse; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.io.SerializedString; -import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.cbor.CBORTestBase; -import com.fasterxml.jackson.dataformat.cbor.testutil.ThrottledInputStream; // [dataformats-binary#728]: `nextFieldName(SerializableString)` used to consume // an entry of expected-length (definite-length) Object twice, when its // name-matching fast path did not succeed, truncating the Object public class NextFieldName728Test extends CBORTestBase { - private final CBORFactory F = new CBORFactory(); - // {"a":1, "b":2} as definite-length Object; name not matched public void testNonMatchingNameDefiniteLength() throws Exception { final byte[] DOC = twoEntryObject(false); - for (boolean stream : new boolean[] { false, true }) { - try (JsonParser p = _parser(DOC, stream)) { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(DOC, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertFalse(p.nextFieldName(new SerializedString("zzz"))); assertToken(JsonToken.FIELD_NAME, p.currentToken()); @@ -47,8 +42,8 @@ public void testNonMatchingSecondNameDefiniteLength() throws Exception { final byte[] DOC = twoEntryObject(false); - for (boolean stream : new boolean[] { false, true }) { - try (JsonParser p = _parser(DOC, stream)) { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(DOC, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertTrue(p.nextFieldName(new SerializedString("a"))); assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); @@ -70,7 +65,7 @@ public void testMatchingNameDefiniteLengthThrottled() throws Exception { final byte[] DOC = twoEntryObject(false); - try (JsonParser p = _parser(DOC, true)) { + try (JsonParser p = cborParser(DOC, true)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertTrue(p.nextFieldName(new SerializedString("a"))); assertEquals(1, p.nextIntValue(-1)); @@ -91,8 +86,8 @@ public void testNonMatchingNameSingleEntry() throws Exception bytes.write(0x01); final byte[] DOC = bytes.toByteArray(); - for (boolean stream : new boolean[] { false, true }) { - try (JsonParser p = _parser(DOC, stream)) { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(DOC, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertFalse(p.nextFieldName(new SerializedString("zzz"))); assertToken(JsonToken.FIELD_NAME, p.currentToken()); @@ -117,8 +112,8 @@ public void testNonMatchingNameNested() throws Exception bytes.write(0x02); final byte[] DOC = bytes.toByteArray(); - for (boolean stream : new boolean[] { false, true }) { - try (JsonParser p = _parser(DOC, stream)) { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(DOC, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertFalse(p.nextFieldName(new SerializedString("zzz"))); assertEquals("a", p.currentName()); @@ -136,14 +131,78 @@ public void testNonMatchingNameNested() throws Exception } } + // Fast path is also skipped for names that are not (definite-length) Strings; + // all of these cases must decode the name without losing the second entry. + // First: name as indefinite-length (chunked) String + public void testChunkedNameDefiniteLength() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(0xA2); // Object, 2 entries + bytes.write(0x7F); // indefinite-length text... + bytes.write(0x61); bytes.write('a'); + bytes.write(0x61); bytes.write('b'); + bytes.write(0xFF); // ... up to break + bytes.write(0x01); + bytes.write(0x61); bytes.write('b'); + bytes.write(0x02); + + _testNonTextName(bytes.toByteArray(), "ab"); + } + + // Second: name as Integer (CBOR allows non-String Object keys) + public void testIntegerNameDefiniteLength() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(0xA2); // Object, 2 entries + bytes.write(0x01); // name: 1 + bytes.write(0x01); + bytes.write(0x61); bytes.write('b'); + bytes.write(0x02); + + _testNonTextName(bytes.toByteArray(), "1"); + } + + // Third: tag-prefixed name (fast path only handles untagged text) + public void testTaggedNameDefiniteLength() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(0xA2); // Object, 2 entries + bytes.write(0xC0); // tag 0 (date/time String) + bytes.write(0x62); bytes.write('a'); bytes.write('b'); + bytes.write(0x01); + bytes.write(0x61); bytes.write('b'); + bytes.write(0x02); + + _testNonTextName(bytes.toByteArray(), "ab"); + } + + private void _testNonTextName(byte[] doc, String expName) throws Exception + { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(doc, throttled)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertToken(JsonToken.FIELD_NAME, p.currentToken()); + assertEquals(expName, p.currentName()); + assertEquals(1, p.nextIntValue(-1)); + // second entry must NOT be lost: + assertToken(JsonToken.FIELD_NAME, p.nextToken()); + assertEquals("b", p.currentName()); + assertEquals(2, p.nextIntValue(-1)); + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + } + } + } + // Indefinite-length Object (what `CBORGenerator` writes): was, and // stays, unaffected public void testNonMatchingNameIndefiniteLength() throws Exception { final byte[] DOC = twoEntryObject(true); - for (boolean stream : new boolean[] { false, true }) { - try (JsonParser p = _parser(DOC, stream)) { + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(DOC, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertFalse(p.nextFieldName(new SerializedString("zzz"))); assertToken(JsonToken.FIELD_NAME, p.currentToken()); @@ -158,6 +217,30 @@ public void testNonMatchingNameIndefiniteLength() throws Exception } } + // Also: for indefinite-length Object, name decoding may find the break + // marker instead of a name, and must then close the Object properly + public void testBreakInPlaceOfNameIndefiniteLength() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(0xBF); // start indefinite-length Object + bytes.write(0x61); bytes.write('a'); + bytes.write(0x01); + bytes.write(0xFF); // end indefinite-length Object + final byte[] DOC = bytes.toByteArray(); + + for (boolean throttled : new boolean[] { false, true }) { + try (JsonParser p = cborParser(DOC, throttled)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertTrue(p.nextFieldName(new SerializedString("a"))); + assertEquals(1, p.nextIntValue(-1)); + // no more entries; break marker where name would be + assertFalse(p.nextFieldName(new SerializedString("zzz"))); + assertToken(JsonToken.END_OBJECT, p.currentToken()); + assertNull(p.nextToken()); + } + } + } + // {"a":1, "b":2}, either as definite- or indefinite-length Object private byte[] twoEntryObject(boolean indefinite) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -172,11 +255,4 @@ private byte[] twoEntryObject(boolean indefinite) { return bytes.toByteArray(); } - private JsonParser _parser(byte[] doc, boolean stream) throws Exception { - if (!stream) { - return F.createParser(doc); - } - // read one byte at a time, so fast path cannot peek at name - return F.createParser(new ThrottledInputStream(new ByteArrayInputStream(doc), 1)); - } } From 9e554b6e8f3fa92e78d049d7b37d0c5c526a05be Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 14:03:24 -0700 Subject: [PATCH 3/4] Test fixes --- .../jackson/dataformat/cbor/CBORTestBase.java | 16 ++++++++++++++-- .../cbor/parse/NextFieldName727Test.java | 11 ++++++++--- .../cbor/parse/NextFieldName728Test.java | 4 +++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java index 19c62ede2..c5f47c170 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/CBORTestBase.java @@ -51,10 +51,22 @@ protected CBORParser cborParser(CBORFactory f, byte[] input) throws IOException * @since 2.18.10 */ protected CBORParser cborParser(byte[] input, boolean throttled) throws IOException { + return cborParser(cborFactory(), input, throttled); + } + + /** + * Variant that takes factory to use: needed when multiple parsers are to + * share symbol table (as they do when created by same factory). + * + * @since 2.18.10 + */ + protected CBORParser cborParser(CBORFactory f, byte[] input, boolean throttled) + throws IOException + { if (throttled) { - return cborParser(new ThrottledInputStream(input, 1)); + return cborParser(f, new ThrottledInputStream(input, 1)); } - return cborParser(input); + return cborParser(f, input); } protected CBORParser cborParser(CBORFactory f, InputStream in) throws IOException { diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java index c1ecdf705..59c70cfe5 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName727Test.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.SerializedString; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.cbor.CBORTestBase; // [dataformats-binary#727]: `nextFieldName(SerializableString)` used to confuse @@ -63,12 +64,16 @@ public void testNoFalseMatchForLength23Name() throws Exception // fast path may see (inline length, 1-byte suffix, 2-byte suffix) public void testNameLengths() throws Exception { + // NOTE: same factory for all parsers on purpose, so that later parsers + // will find names decoded by earlier ones from (shared) symbol table + final CBORFactory f = cborFactory(); + for (int len : new int[] { 1, 2, 3, 22, 23, 24, 25, 100, 255, 256, 1000 }) { final String name = repeat('a', len - 1) + 'b'; final String differentName = repeat('a', len - 1) + 'c'; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - try (JsonGenerator g = cborGenerator(bytes)) { + try (JsonGenerator g = cborGenerator(f, bytes)) { g.writeStartObject(); g.writeStringField(name, "v"); g.writeEndObject(); @@ -76,7 +81,7 @@ public void testNameLengths() throws Exception final byte[] doc = bytes.toByteArray(); for (boolean throttled : new boolean[] { false, true }) { - try (JsonParser p = cborParser(doc, throttled)) { + try (JsonParser p = cborParser(f, doc, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertTrue("Should match name of length "+len+" (throttled? "+throttled+")", p.nextFieldName(new SerializedString(name))); @@ -85,7 +90,7 @@ public void testNameLengths() throws Exception assertToken(JsonToken.END_OBJECT, p.nextToken()); assertNull(p.nextToken()); } - try (JsonParser p = cborParser(doc, throttled)) { + try (JsonParser p = cborParser(f, doc, throttled)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertFalse("Should not match different name of length "+len+" (throttled? "+throttled+")", p.nextFieldName(new SerializedString(differentName))); diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java index a74264506..46674a3c1 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/NextFieldName728Test.java @@ -167,7 +167,9 @@ public void testTaggedNameDefiniteLength() throws Exception { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); bytes.write(0xA2); // Object, 2 entries - bytes.write(0xC0); // tag 0 (date/time String) + // Tag 55799 ("Self-Described CBOR"): semantics do not constrain the + // tagged value, unlike f.ex tag 0 (date/time String) + bytes.write(0xD9); bytes.write(0xD9); bytes.write(0xF7); bytes.write(0x62); bytes.write('a'); bytes.write('b'); bytes.write(0x01); bytes.write(0x61); bytes.write('b'); From 6f0c58459882e4ef2ec2dcba4c141cc7f54d149b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 14:07:11 -0700 Subject: [PATCH 4/4] Last minor simplification --- .../fasterxml/jackson/dataformat/cbor/CBORParser.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 686a0cad8..003f78067 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 @@ -1458,13 +1458,9 @@ public boolean nextFieldName(SerializableString str) throws IOException // `nextToken()` here: we have already consumed one entry of // expected-length Object (`expectMoreValues()` above) and // `nextToken()` would consume another one. So decode name here. - final JsonToken t = _decodePropertyName(); - // Note: should not get `null` (for end-of-input) since we are within - // Object and `_handleEOF()` fails for that case; but just in case: - if (t == null) { - return false; - } - return (_updateToken(t) == JsonToken.FIELD_NAME) + // Note: cannot get `null` for end-of-input here since we are + // within Object, and `_handleEOF()` fails for that case. + return (_updateToken(_decodePropertyName()) == JsonToken.FIELD_NAME) && str.getValue().equals(getCurrentName()); } // otherwise just fall back to default handling; should occur rarely