feat(gb32960/vendor): add TLV catch-all parser for Guangdong peer 0x83
Same peer that uses the Guangdong fuel-cell extension also emits a proprietary 0x83 block that is NOT in the published v1.0 spec table 8. Empirically it follows a TLV layout: type(1) + length(2) + value(N). Adds: - InfoBlock.GuangdongFc.VendorTlv(typeCode, declaredLength, payload) record + permits clause + InfoBlockType.GD_FC_VENDOR_TLV enum entry. - GdFcVendorTlvBlockParser: typeCode injected via constructor, reads 2B big-endian length then exactly that many payload bytes. Defends against declaredLength > remaining by truncating to remaining. - AutoConfig: registers `new GdFcVendorTlvBlockParser(0x83)` under the guangdong-fc catalog entry. Future unknown vendor typeCodes can be added by inserting more instances; no new class required. Critical correctness property — the parser is strictly bounded by the length field, so BodyParser's main loop continues processing blocks that come AFTER the TLV instead of having them swallowed by the old generic Raw fallback. Three unit tests cover this: - parsesPayloadOfDeclaredLengthExactly: round-trip a 5-byte payload - doesNotSwallowSubsequentBlocksWhenChainedInBodyParser: feeds a body containing 0x83 TLV(4B) followed by a 0x01 standard Vehicle block; asserts both blocks are parsed - truncatesPayloadIfDeclaredLengthExceedsRemaining: defensive case Test count: 32 -> 35. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong;
|
||||
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960BodyParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960MessageDecoder;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParserRegistry;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.v2016.VehicleV2016BlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.model.InfoBlock;
|
||||
import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 验证 {@link GdFcVendorTlvBlockParser} 严格按 length 消费字节,不会贪心吞光后续信息体。
|
||||
*
|
||||
* <p>这是关键回归 case:用户问"加 0x83 兜底会不会把后面其它块也吃掉?"——通过构造
|
||||
* 一个 {@code 0x83 TLV + 0x01 Vehicle} 串联的 body,断言 BodyParser 解出**两个块**而非一个。
|
||||
*/
|
||||
class GdFcVendorTlvBlockParserTest {
|
||||
|
||||
@Test
|
||||
void parsesPayloadOfDeclaredLengthExactly() {
|
||||
// 0x83 typeCode 已被外层消费,buffer 从 length 字段开始
|
||||
byte[] data = bytes(
|
||||
0x00, 0x05, // length = 5
|
||||
0xAA, 0xBB, 0xCC, 0xDD, 0xEE); // 5 bytes payload
|
||||
ByteBuffer buf = ByteBuffer.wrap(data);
|
||||
|
||||
InfoBlock.GuangdongFc.VendorTlv tlv =
|
||||
(InfoBlock.GuangdongFc.VendorTlv) new GdFcVendorTlvBlockParser(0x83).parse(buf);
|
||||
|
||||
assertThat(tlv.typeCode()).isEqualTo(0x83);
|
||||
assertThat(tlv.declaredLength()).isEqualTo(5);
|
||||
assertThat(tlv.payload()).containsExactly(0xAA, 0xBB, 0xCC, 0xDD, 0xEE);
|
||||
assertThat(buf.remaining()).as("应正好消费 2+5=7 字节").isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotSwallowSubsequentBlocksWhenChainedInBodyParser() {
|
||||
// 构造 body:0x83 TLV(length=4) + 0x01 Vehicle(20B 标准布局)
|
||||
ByteArrayOutputStream body = new ByteArrayOutputStream();
|
||||
// 0x83 TLV:3+4 = 7 字节
|
||||
body.write(0x83);
|
||||
body.write(0x00); body.write(0x04); // length = 4
|
||||
body.write(0x11); body.write(0x22); body.write(0x33); body.write(0x44);
|
||||
// 0x01 Vehicle: typeCode + 20 字节 body
|
||||
body.write(0x01);
|
||||
body.write(0x01); // vehicleState
|
||||
body.write(0x03); // chargingState
|
||||
body.write(0x02); // runningMode
|
||||
write16(body, 0); // speed
|
||||
write32(body, 100_000L); // mileage
|
||||
write16(body, 5605); // totalVoltage
|
||||
write16(body, 10000); // totalCurrent
|
||||
body.write(85); // SOC
|
||||
body.write(0x01); // dcDcStatus
|
||||
body.write(0x00); // gear
|
||||
write16(body, 10000); // insulation
|
||||
body.write(0x00); // accelPedal
|
||||
body.write(0x00); // brakePedal
|
||||
|
||||
// 装配 body parser:注册 0x83 vendor TLV + 0x01 标准 Vehicle
|
||||
InfoBlockParserRegistry registry = new InfoBlockParserRegistry(List.of(
|
||||
new GdFcVendorTlvBlockParser(0x83),
|
||||
new VehicleV2016BlockParser()));
|
||||
Gb32960BodyParser parser = new Gb32960BodyParser(registry);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.wrap(body.toByteArray());
|
||||
Gb32960MessageDecoder.BodyParseResult result = parser.parse(ProtocolVersion.V2016, buf);
|
||||
|
||||
// 关键断言:两个块都解析出来了,0x01 没被 0x83 吞掉
|
||||
assertThat(result.blocks()).hasSize(2);
|
||||
assertThat(result.blocks().get(0)).isInstanceOf(InfoBlock.GuangdongFc.VendorTlv.class);
|
||||
assertThat(result.blocks().get(1)).isInstanceOf(InfoBlock.Gb32960V2016.Vehicle.class);
|
||||
|
||||
InfoBlock.GuangdongFc.VendorTlv tlv = (InfoBlock.GuangdongFc.VendorTlv) result.blocks().get(0);
|
||||
assertThat(tlv.declaredLength()).isEqualTo(4);
|
||||
assertThat(tlv.payload()).containsExactly(0x11, 0x22, 0x33, 0x44);
|
||||
|
||||
InfoBlock.Gb32960V2016.Vehicle v = (InfoBlock.Gb32960V2016.Vehicle) result.blocks().get(1);
|
||||
assertThat(v.socPercent()).isEqualTo(85);
|
||||
assertThat(v.totalVoltageV()).isEqualTo(560.5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void truncatesPayloadIfDeclaredLengthExceedsRemaining() {
|
||||
// 防御场景:length 字段声称 100 字节但 buffer 只剩 3 字节
|
||||
byte[] data = bytes(0x00, 0x64, 0x01, 0x02, 0x03);
|
||||
ByteBuffer buf = ByteBuffer.wrap(data);
|
||||
InfoBlock.GuangdongFc.VendorTlv tlv =
|
||||
(InfoBlock.GuangdongFc.VendorTlv) new GdFcVendorTlvBlockParser(0x83).parse(buf);
|
||||
assertThat(tlv.declaredLength()).isEqualTo(100);
|
||||
assertThat(tlv.payload()).hasSize(3).containsExactly(0x01, 0x02, 0x03);
|
||||
}
|
||||
|
||||
private static byte[] bytes(int... values) {
|
||||
byte[] out = new byte[values.length];
|
||||
for (int i = 0; i < values.length; i++) out[i] = (byte) values[i];
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void write16(ByteArrayOutputStream os, int v) {
|
||||
os.write((v >> 8) & 0xFF);
|
||||
os.write(v & 0xFF);
|
||||
}
|
||||
|
||||
private static void write32(ByteArrayOutputStream os, long v) {
|
||||
os.write((int) ((v >> 24) & 0xFF));
|
||||
os.write((int) ((v >> 16) & 0xFF));
|
||||
os.write((int) ((v >> 8) & 0xFF));
|
||||
os.write((int) (v & 0xFF));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user