diff --git a/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/ProtobufParser.java b/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/ProtobufParser.java index dee473f1b..8baaeefd6 100644 --- a/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/ProtobufParser.java @@ -763,7 +763,10 @@ private JsonToken _handleRootKey(int tag) throws IOException } // array? if (f.repeated) { - if (f.packed) { + // 03-Jul-2026, tatu: [dataformats-binary#134] Decide packed-vs-unpacked from + // the actual wire type, not just the schema's declared `packed` flag: + // proto3 permits either encoding for repeated scalar/enum fields. + if (f.isPackedInWire(wireType)) { _state = STATE_ARRAY_START_PACKED; } else { _state = STATE_ARRAY_START; @@ -807,7 +810,10 @@ private JsonToken _handleNestedKey(int tag) throws IOException // array? if (f.repeated) { - if (f.packed) { + // 03-Jul-2026, tatu: [dataformats-binary#134] Decide packed-vs-unpacked from + // the actual wire type, not just the schema's declared `packed` flag: + // proto3 permits either encoding for repeated scalar/enum fields. + if (f.isPackedInWire(wireType)) { _state = STATE_ARRAY_START_PACKED; } else { _state = STATE_ARRAY_START; @@ -1074,7 +1080,10 @@ public boolean nextFieldName(SerializableString sstr) throws IOException // array? if (_currentField.repeated) { - if (_currentField.packed) { + // 03-Jul-2026, tatu: [dataformats-binary#134] Decide packed-vs-unpacked from + // the actual wire type, not just the schema's declared `packed` flag: + // proto3 permits either encoding for repeated scalar/enum fields. + if (_currentField.isPackedInWire(wireType)) { _state = STATE_ARRAY_START_PACKED; } else { _state = STATE_ARRAY_START; @@ -1110,7 +1119,10 @@ public boolean nextFieldName(SerializableString sstr) throws IOException // array? if (_currentField.repeated) { - if (_currentField.packed) { + // 03-Jul-2026, tatu: [dataformats-binary#134] Decide packed-vs-unpacked from + // the actual wire type, not just the schema's declared `packed` flag: + // proto3 permits either encoding for repeated scalar/enum fields. + if (_currentField.isPackedInWire(wireType)) { _state = STATE_ARRAY_START_PACKED; } else { _state = STATE_ARRAY_START; @@ -1159,7 +1171,10 @@ public String nextFieldName() throws IOException // array? if (_currentField.repeated) { - if (_currentField.packed) { + // 03-Jul-2026, tatu: [dataformats-binary#134] Decide packed-vs-unpacked from + // the actual wire type, not just the schema's declared `packed` flag: + // proto3 permits either encoding for repeated scalar/enum fields. + if (_currentField.isPackedInWire(wireType)) { _state = STATE_ARRAY_START_PACKED; } else { _state = STATE_ARRAY_START; @@ -1198,7 +1213,10 @@ public String nextFieldName() throws IOException // array? if (_currentField.repeated) { - if (_currentField.packed) { + // 03-Jul-2026, tatu: [dataformats-binary#134] Decide packed-vs-unpacked from + // the actual wire type, not just the schema's declared `packed` flag: + // proto3 permits either encoding for repeated scalar/enum fields. + if (_currentField.isPackedInWire(wireType)) { _state = STATE_ARRAY_START_PACKED; } else { _state = STATE_ARRAY_START; diff --git a/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/schema/ProtobufField.java b/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/schema/ProtobufField.java index 8a0e92c67..b0a4380e0 100644 --- a/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/schema/ProtobufField.java +++ b/protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/schema/ProtobufField.java @@ -221,7 +221,34 @@ public final boolean isArray() { public final boolean isValidFor(int typeTag) { return (typeTag == wireType) // 13-Apr-2017, tatu: to fix [dataformats-binary#76] - || (packed && repeated && typeTag == WireType.LENGTH_PREFIXED); + // 03-Jul-2026, tatu: [dataformats-binary#134] A repeated scalar/enum + // field may arrive packed (LENGTH_PREFIXED) regardless of the schema's + // declared `packed` flag -- proto3 permits either encoding on the wire, + // so tolerance must key off the type, not the schema default. + || (repeated && type.isPackable() && typeTag == WireType.LENGTH_PREFIXED); + } + + /** + * Accessor for deciding whether an incoming, repeated field should be read + * using "packed" (single length-prefixed block) encoding. + *
+ * For genuinely packable types (scalar numeric/enum/boolean) the native + * unpacked wire type differs from {@code LENGTH_PREFIXED}, so the actual wire + * type is unambiguous and authoritative: proto3 permits either encoding on the + * wire regardless of the schema's declared {@code packed} flag. + *
+ * For non-packable types (String/Bytes/Message) a single element and a + * jackson-style "packed" block are both {@code LENGTH_PREFIXED}, so the + * wire type cannot distinguish them; there we must fall back to the schema's + * declared {@code packed} flag. + * + * @since 2.21.5 [dataformats-binary#134] + */ + public final boolean isPackedInWire(int typeTag) { + if (type.isPackable()) { + return repeated && (typeTag == WireType.LENGTH_PREFIXED); + } + return packed; } @Override diff --git a/protobuf/src/test/java/com/fasterxml/jackson/dataformat/protobuf/Proto3PackedDefault134Test.java b/protobuf/src/test/java/com/fasterxml/jackson/dataformat/protobuf/Proto3PackedDefault134Test.java index 4dd9a39dc..d07560cdc 100644 --- a/protobuf/src/test/java/com/fasterxml/jackson/dataformat/protobuf/Proto3PackedDefault134Test.java +++ b/protobuf/src/test/java/com/fasterxml/jackson/dataformat/protobuf/Proto3PackedDefault134Test.java @@ -116,4 +116,72 @@ public void testProto3WriteDefaultsToPacked() throws Exception assertEquals(100, t.get("f").get(0).asInt()); assertEquals(200, t.get("f").get(1).asInt()); } + + // [dataformats-binary#134]: even though a proto3 schema declares the field + // "packed" by default, the wire is authoritative -- an unpacked proto3 stream + // (legal per spec) must still decode. Decoder keys off the actual wire type. + @Test + public void testProto3PackedSchemaStillReadsUnpackedWire() throws Exception + { + final String SCHEMA_STR = "syntax = \"proto3\";\n" + + "message t {\n" + + " repeated uint32 f = 1;\n" + + "}\n"; + // unpacked encoding despite proto3 default being packed + final byte[] pb = { 0x8, 0x64, 0x8, (byte) 0xc8, 0x1 }; // f = [100, 200], unpacked + + ProtobufSchema schema = ProtobufSchemaLoader.std.parse(SCHEMA_STR); + JsonNode t = MAPPER.readerFor(JsonNode.class).with(schema).readValue(pb); + + assertEquals(2, t.get("f").size()); + assertEquals(100, t.get("f").get(0).asInt()); + assertEquals(200, t.get("f").get(1).asInt()); + } + + // [dataformats-binary#134]: conversely, an unpacked-by-default schema (proto2, + // or proto3 with `[packed=false]`) must still decode a packed wire stream. + @Test + public void testUnpackedSchemaStillReadsPackedWire() throws Exception + { + final String SCHEMA_STR = "syntax = \"proto2\";\n" + + "message t {\n" + + " repeated uint32 f = 1;\n" + + "}\n"; + // packed encoding despite proto2 default being unpacked + final byte[] pb = { 0xa, 0x3, 0x64, (byte) 0xc8, 0x1 }; // f = [100, 200], packed + + ProtobufSchema schema = ProtobufSchemaLoader.std.parse(SCHEMA_STR); + JsonNode t = MAPPER.readerFor(JsonNode.class).with(schema).readValue(pb); + + assertEquals(2, t.get("f").size()); + assertEquals(100, t.get("f").get(0).asInt()); + assertEquals(200, t.get("f").get(1).asInt()); + } + + // [dataformats-binary#134]: mismatch tolerance must also work for a repeated + // field nested inside a message (exercises `_handleNestedKey`, not just root). + // Uses proto2 + explicit `[packed=true]` to declare the nested field packed + // while feeding an unpacked wire stream (the old square protoparser does not + // accept proto3 singular message fields, so we avoid proto3 syntax here). + @Test + public void testNestedRepeatedPackedSchemaReadsUnpackedWire() throws Exception + { + final String SCHEMA_STR = "message Outer {\n" + + " optional Inner inner = 1;\n" + + "}\n" + + "message Inner {\n" + + " repeated uint32 f = 1 [packed=true];\n" + + "}\n"; + // Outer.inner (field 1, length-delimited) wrapping Inner with unpacked f=[100,200] + // inner payload: 0x8,0x64, 0x8,0xc8,0x1 (5 bytes) + final byte[] pb = { 0xa, 0x5, 0x8, 0x64, 0x8, (byte) 0xc8, 0x1 }; + + ProtobufSchema schema = ProtobufSchemaLoader.std.parse(SCHEMA_STR, "Outer"); + JsonNode t = MAPPER.readerFor(JsonNode.class).with(schema).readValue(pb); + + JsonNode arr = t.get("inner").get("f"); + assertEquals(2, arr.size()); + assertEquals(100, arr.get(0).asInt()); + assertEquals(200, arr.get(1).asInt()); + } }