Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions pkg/decoder/tagsl/v1/configuration_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ func MatchConfiguration(sentHex, observedHex string) (bool, error) {
return configurationMatches(sentPayload, observedPayload), nil
}

// ValidateConfiguration reports whether sent is a well-formed port-128 configuration downlink.
func ValidateConfiguration(sentHex string) error {
sent, err := common.HexStringToBytes(sentHex)
if err != nil {
return err
}
if len(sent) != configurationSentByteLength {
return fmt.Errorf("%w: sent configuration payload length %d", common.ErrInvalidPayloadLength, len(sent))
}
_, err = decodePort128Payload(sentHex)
return err
}

func validConfigurationReportLength(n int) bool {
switch n {
case configurationReportCoreByteLength, configurationReportWithBatchByteLength, configurationReportFullByteLength:
Expand Down Expand Up @@ -140,6 +153,19 @@ func MatchBleConfiguration(sentHex, observedHex string) (bool, error) {
return sentPayload == observedPayload, nil
}

// ValidateBleConfiguration reports whether sent is a well-formed port-134 BLE configuration downlink.
func ValidateBleConfiguration(sentHex string) error {
sent, err := common.HexStringToBytes(sentHex)
if err != nil {
return err
}
if len(sent) != bleConfigurationByteLength {
return fmt.Errorf("%w: sent BLE configuration payload length %d", common.ErrInvalidPayloadLength, len(sent))
}
_, err = decodePort8Payload(sentHex)
return err
}

func decodePort8Payload(payloadHex string) (Port8Payload, error) {
config := port8PayloadConfig()
decoded, err := common.Decode(&payloadHex, &config)
Expand Down
34 changes: 34 additions & 0 deletions pkg/decoder/tagsl/v1/configuration_match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,40 @@ func assertConfigurationError(t *testing.T, err error, target error) {
}
}

func TestValidateConfiguration(t *testing.T) {
if err := ValidateConfiguration(tagSLSentHex); err != nil {
t.Fatalf("expected valid sent payload: %v", err)
}

if err := ValidateConfiguration("zz"); err == nil {
t.Fatal("expected hex error")
}

err := ValidateConfiguration(tagSLSentHex[:len(tagSLSentHex)-2])
assertConfigurationError(t, err, common.ErrInvalidPayloadLength)

sent := mustMutateHex(t, tagSLSentHex, map[int][]byte{3: {0x00, 0x00, 0x00, 0x04}})
if err := ValidateConfiguration(sent); !errors.Is(err, common.ErrValidationFailed) {
t.Fatalf("expected validation error, got %v", err)
}
}

func TestValidateBleConfiguration(t *testing.T) {
if err := ValidateBleConfiguration(tagSLBleSentHex); err != nil {
t.Fatalf("expected valid BLE payload: %v", err)
}

if err := ValidateBleConfiguration("zz"); err == nil {
t.Fatal("expected hex error")
}

err := ValidateBleConfiguration(tagSLBleSentHex[:len(tagSLBleSentHex)-2])
assertConfigurationError(t, err, common.ErrInvalidPayloadLength)

err = ValidateBleConfiguration(tagSLBleSentHex + "00")
assertConfigurationError(t, err, common.ErrInvalidPayloadLength)
}

func mustMutateHex(t *testing.T, payloadHex string, replacements map[int][]byte) string {
t.Helper()
payload, err := hex.DecodeString(payloadHex)
Expand Down
18 changes: 17 additions & 1 deletion pkg/decoder/tagxl/v1/configuration_dialect.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,18 @@ type dialectSpec struct {
limits dialectLimits
setters map[byte]commandSpec
getters map[byte]commandSpec
actions map[byte]int
}

func newDialectSpec(limits dialectLimits, commands []commandSpec) dialectSpec {
func newDialectSpec(limits dialectLimits, commands []commandSpec, actions map[byte]int) dialectSpec {
spec := dialectSpec{
limits: limits,
setters: make(map[byte]commandSpec, len(commands)),
getters: make(map[byte]commandSpec, len(commands)),
actions: actions,
}
if spec.actions == nil {
spec.actions = map[byte]int{}
}
for _, command := range commands {
if command.setterTag != 0 {
Expand Down Expand Up @@ -120,6 +125,12 @@ var tagXLDialect = newDialectSpec(dialectLimits{maxCommands: 32, maxResponseByte
{getterTag: tlvTagScanCounts, responseLen: 4},
{getterTag: 0x4c, responseLen: 4},
{getterTag: 0x52, responseLen: 6},
}, map[byte]int{
actionTagAlarm: 2,
actionTagResetDevice: 0,
actionTagScanNow: 0,
actionTagClearStorage: 0,
actionTagWipeAll: 0,
})

var smartLabelV2Dialect = newDialectSpec(dialectLimits{maxCommands: 30, maxResponseBytes: 51}, []commandSpec{
Expand All @@ -141,6 +152,11 @@ var smartLabelV2Dialect = newDialectSpec(dialectLimits{maxCommands: 30, maxRespo
{getterTag: 0x4b, responseLen: 4},
{getterTag: 0x61, responseLen: 1},
{getterTag: 0x63, responseLen: 2},
}, map[byte]int{
actionTagResetDevice: 0,
actionTagScanNow: 0,
actionTagClearStorage: 0,
actionTagWipeAll: 0,
})

func dialectSpecFor(dialect ConfigurationDialect) (dialectSpec, error) {
Expand Down
82 changes: 59 additions & 23 deletions pkg/decoder/tagxl/v1/configuration_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ type configurationRequirement struct {
wantValue []byte
}

type sentConfiguration struct {
requirements map[byte]configurationRequirement
hasActions bool
}

// MatchConfiguration reports whether observed reflects every setter in sent
// using the Tag XL dialect.
func MatchConfiguration(sentHex, observedHex string) (bool, error) {
Expand Down Expand Up @@ -77,21 +82,24 @@ func CompareConfigurationFor(dialect ConfigurationDialect, sentHex, observedHex
return 0, err
}

requirements, err := parseConfigurationSent(spec, sent)
parsed, err := parseConfigurationSent(spec, sent)
if err != nil {
return 0, err
}
seen, err := parseConfigurationObserved(requirements, observed)
if len(parsed.requirements) == 0 {
return 0, errConfigurationNoSetter
}
seen, err := parseConfigurationObserved(parsed.requirements, observed)
if err != nil {
return 0, err
}

for tag := range requirements {
for tag := range parsed.requirements {
if _, ok := seen[tag]; !ok {
return ConfigurationIncomplete, nil
}
}
for tag, requirement := range requirements {
for tag, requirement := range parsed.requirements {
if requirement.wantValue == nil {
continue
}
Expand All @@ -104,17 +112,29 @@ func CompareConfigurationFor(dialect ConfigurationDialect, sentHex, observedHex
return ConfigurationMatch, nil
}

// ValidateConfiguration reports whether sent is a verifiable payload for dialect.
func ValidateConfiguration(dialect ConfigurationDialect, sentHex string) error {
// AnalyzeConfiguration classifies a sent payload without comparing an uplink.
// hasObservableRequirements is true when at least one setter or getter is present.
// hasActions is true when at least one supported action command is present.
func AnalyzeConfiguration(dialect ConfigurationDialect, sentHex string) (hasObservableRequirements, hasActions bool, err error) {
spec, err := dialectSpecFor(dialect)
if err != nil {
return err
return false, false, err
}
sent, err := common.HexStringToBytes(sentHex)
if err != nil {
return err
return false, false, err
}
parsed, err := parseConfigurationSent(spec, sent)
if err != nil {
return false, false, err
}
_, err = parseConfigurationSent(spec, sent)
return len(parsed.requirements) > 0, parsed.hasActions, nil
}

// ValidateConfiguration reports whether sent is a syntactically valid payload for dialect.
// Action-only payloads are valid.
func ValidateConfiguration(dialect ConfigurationDialect, sentHex string) error {
_, _, err := AnalyzeConfiguration(dialect, sentHex)
return err
}

Expand All @@ -128,47 +148,63 @@ func BuildCurrentConfigurationRequest(dialect ConfigurationDialect, sentHex stri
if err != nil {
return nil, err
}
requirements, err := parseConfigurationSent(spec, sent)
parsed, err := parseConfigurationSent(spec, sent)
if err != nil {
return nil, err
}
if len(parsed.requirements) == 0 {
return nil, errConfigurationNoSetter
}

payload := []byte{configurationEnvelopeMarker, 0x00, 0x00}
for _, requirement := range orderedRequirements(sent, spec, requirements) {
for _, requirement := range orderedRequirements(sent, spec, parsed.requirements) {
payload = append(payload, requirement.spec.getterTag, 0x00)
}
payload[2] = byte((len(payload) - 3) / 2)
payload[1] = byte(len(payload) - 2)
return payload, nil
}

func parseConfigurationSent(spec dialectSpec, payload []byte) (map[byte]configurationRequirement, error) {
func parseConfigurationSent(spec dialectSpec, payload []byte) (sentConfiguration, error) {
if len(payload) < 3 || payload[0] != configurationEnvelopeMarker {
return nil, errConfigurationWrongMarker
return sentConfiguration{}, errConfigurationWrongMarker
}

requirements := make(map[byte]configurationRequirement)
seenActions := make(map[byte]struct{})
commandCount := 0
offset := 3
for offset < len(payload) {
tag, value, next, err := readTLV(payload, offset)
if err != nil {
return nil, err
return sentConfiguration{}, err
}
commandCount++
if commandCount > spec.limits.maxCommands {
return nil, errConfigurationTooManyCommands
return sentConfiguration{}, errConfigurationTooManyCommands
}

if actionLen, ok := spec.actions[tag]; ok {
if len(value) != actionLen {
return sentConfiguration{}, errConfigurationMalformedTLV
}
if _, exists := seenActions[tag]; exists {
return sentConfiguration{}, errConfigurationDuplicateTag
}
seenActions[tag] = struct{}{}
offset = next
continue
}

if setter, ok := spec.setters[tag]; ok {
if len(value) != setter.setterLen {
return nil, errConfigurationMalformedTLV
return sentConfiguration{}, errConfigurationMalformedTLV
}
if err := validateCommandValue(setter, value); err != nil {
return nil, err
return sentConfiguration{}, err
}
if existing, exists := requirements[setter.getterTag]; exists && existing.wantValue != nil {
return nil, errConfigurationDuplicateTag
return sentConfiguration{}, errConfigurationDuplicateTag
}
copied := append([]byte(nil), value...)
requirements[setter.getterTag] = configurationRequirement{spec: setter, wantValue: copied}
Expand All @@ -178,21 +214,21 @@ func parseConfigurationSent(spec dialectSpec, payload []byte) (map[byte]configur

getter, ok := spec.getters[tag]
if !ok || len(value) != 0 {
return nil, errConfigurationUnsupportedCommand
return sentConfiguration{}, errConfigurationUnsupportedCommand
}
if _, exists := requirements[getter.getterTag]; !exists {
requirements[getter.getterTag] = configurationRequirement{spec: getter}
}
offset = next
}

if len(requirements) == 0 {
return nil, errConfigurationNoSetter
if len(requirements) == 0 && len(seenActions) == 0 {
return sentConfiguration{}, errConfigurationNoSetter
}
if expectedResponseSize(requirements) > spec.limits.maxResponseBytes {
return nil, errConfigurationTooLarge
return sentConfiguration{}, errConfigurationTooLarge
}
return requirements, nil
return sentConfiguration{requirements: requirements, hasActions: len(seenActions) > 0}, nil
}

func parseConfigurationObserved(requirements map[byte]configurationRequirement, payload []byte) (map[byte][][]byte, error) {
Expand Down
Loading
Loading