diff --git a/pkg/common/helpers.go b/pkg/common/helpers.go index fb03580..cb15945 100644 --- a/pkg/common/helpers.go +++ b/pkg/common/helpers.go @@ -11,6 +11,8 @@ import ( "time" "github.com/go-playground/validator" + "github.com/truvami/decoder/internal/logger" + "go.uber.org/zap" ) func HexStringToBytes(hexString string) ([]byte, error) { @@ -154,21 +156,28 @@ func Decode(payloadHex *string, config *PayloadConfig) (any, error) { errs := []error{} if len(config.Tags) != 0 { - var index uint8 = 3 - var payloadLength = uint8(len(payloadBytes)) - for index+2 < payloadLength { - var found = false + var index = 3 + var payloadLength = len(payloadBytes) + for index < payloadLength { + if payloadLength-index < 2 { + return nil, fmt.Errorf("incomplete TLV header at offset %d: need 2 bytes but only %d remain", index, payloadLength-index) + } + var tag = payloadBytes[index] - index++ - var length = payloadBytes[index] - index++ + var length = int(payloadBytes[index+1]) + index += 2 + if index+length > payloadLength { + return nil, fmt.Errorf("TLV tag 0x%02x at offset %d declares length %d, but only %d bytes remain", tag, index-2, length, payloadLength-index) + } + + var found bool for _, tagConfig := range config.Tags { if tagConfig.Tag == tag { found = true config.Features = append(config.Features, tagConfig.Feature) - value, err := extractFieldValue(payloadBytes, int(index), int(length), false, tagConfig.Hex) + value, err := extractFieldValue(payloadBytes, index, length, false, tagConfig.Hex) if err != nil { return nil, err } @@ -198,11 +207,14 @@ func Decode(payloadHex *string, config *PayloadConfig) (any, error) { } } } - if found { - index += length - } else { - return nil, fmt.Errorf("unknown tag %x", tag) + if !found { + tagHex := fmt.Sprintf("0x%02x", tag) + unknownTLVTagsTotal.WithLabelValues(tagHex).Inc() + if logger.Logger != nil { + logger.Logger.Warn("skipping unknown tag", zap.String("tag", tagHex), zap.Int("length", length)) + } } + index += length } return targetValue.Interface(), errors.Join(errs...) diff --git a/pkg/common/helpers_test.go b/pkg/common/helpers_test.go index 73aa45e..fd086be 100644 --- a/pkg/common/helpers_test.go +++ b/pkg/common/helpers_test.go @@ -125,10 +125,11 @@ func TestDecode(t *testing.T) { payload: "ffffff0000", config: tagConfig, expected: Port2Payload{ - Time: nil, + Time: Uint32Ptr(0), Power: nil, Sensor: nil, }, + expectedErr: "validation failed for Time", }, { payload: "ffffff000400000000", @@ -141,16 +142,19 @@ func TestDecode(t *testing.T) { expectedErr: "validation failed for Time", }, { - payload: "ffffff040100", - config: tagConfig, - expected: nil, - expectedErr: "unknown tag 4", + payload: "ffffff040100", + config: tagConfig, + expected: Port2Payload{ + Time: nil, + Power: nil, + Sensor: nil, + }, }, { payload: "ffffff010200", config: tagConfig, expected: nil, - expectedErr: "field out of bounds", + expectedErr: "TLV tag", }, { payload: "ffffff030100", @@ -163,8 +167,15 @@ func TestDecode(t *testing.T) { for _, test := range tests { t.Run(test.payload, func(t *testing.T) { decodedData, err := Decode(StringPtr(test.payload), &test.config) - if err != nil && !strings.Contains(err.Error(), test.expectedErr) { - t.Fatalf("expected %s received %s", test.expectedErr, err) + if test.expectedErr != "" { + if err == nil { + t.Fatalf("expected error containing %q but got nil", test.expectedErr) + } + if !strings.Contains(err.Error(), test.expectedErr) { + t.Fatalf("expected error containing %q received %s", test.expectedErr, err) + } + } else if err != nil { + t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(decodedData, test.expected) { t.Fatalf("expected: %+v received: %+v", test.expected, decodedData) @@ -239,8 +250,15 @@ func TestExtractFieldValue(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("%v_%v_%v", test.payload, test.start, test.length), func(t *testing.T) { result, err := extractFieldValue(test.payload, test.start, test.length, test.optional, test.hexadecimal) - if err != nil && err.Error() != test.expectedErr { - t.Fatalf("expected: %s received: %s", test.expectedErr, err.Error()) + if test.expectedErr != "" { + if err == nil { + t.Fatalf("expected error %q but got nil", test.expectedErr) + } + if err.Error() != test.expectedErr { + t.Fatalf("expected: %s received: %s", test.expectedErr, err.Error()) + } + } else if err != nil { + t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(result, test.expected) { t.Fatalf("expected: %v received: %v", test.expected, result) @@ -373,8 +391,15 @@ func TestConvertFieldValue(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("%v_%v", test.value, test.fieldType), func(t *testing.T) { result, err := convertFieldValue(test.value, test.fieldType, nil) - if err != nil && err.Error() != test.expectedErr { - t.Fatalf("expected: %s received: %s", test.expectedErr, err.Error()) + if test.expectedErr != "" { + if err == nil { + t.Fatalf("expected error %q but got nil", test.expectedErr) + } + if err.Error() != test.expectedErr { + t.Fatalf("expected: %s received: %s", test.expectedErr, err.Error()) + } + } else if err != nil { + t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(result, test.expected) { t.Fatalf("expected: %v received: %v", test.expected, result) @@ -450,8 +475,15 @@ func TestInsertFieldBytes(t *testing.T) { for _, test := range tests { set, bytes, err := insertFieldBytes(test.value, test.length, test.transform) - if err != nil && err.Error() != test.expectedErr { - t.Fatalf("expected: %s received: %s", test.expectedErr, err.Error()) + if test.expectedErr != "" { + if err == nil { + t.Fatalf("expected error %q but got nil", test.expectedErr) + } + if err.Error() != test.expectedErr { + t.Fatalf("expected: %s received: %s", test.expectedErr, err.Error()) + } + } else if err != nil { + t.Fatalf("unexpected error: %s", err) } if !set && err == nil { t.Fatalf("expected set to be true when error is nil") diff --git a/pkg/common/metrics.go b/pkg/common/metrics.go new file mode 100644 index 0000000..fee4425 --- /dev/null +++ b/pkg/common/metrics.go @@ -0,0 +1,13 @@ +package common + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + unknownTLVTagsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "truvami_common_unknown_tlv_tags_total", + Help: "The total number of unknown TLV tags encountered during decoding", + }, []string{"tag"}) +) diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index e659531..e3f8750 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -117,10 +117,17 @@ func TestDecode(t *testing.T) { expectedErr: "port not supported: port 151 tag ff", }, { - port: 151, - payload: "4c0501ff020000", - expected: Port151Payload{}, - expectedErr: "unknown tag ff", + port: 151, + payload: "4c0501ff020000", + expected: Port151Payload{}, + }, + { + port: 151, + payload: "4c0d0345020a92ff03aabbcc4e0107", + expected: Port151Payload{ + Battery: helpers.Float32Ptr(2.706), + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLADR), + }, }, { port: 151,