Add fuzz tests for config, remap, and sacn packages

This commit is contained in:
Ian Gulliver
2026-01-28 12:34:32 -08:00
parent 8e24dee91f
commit 416e67b3f1
3 changed files with 381 additions and 0 deletions

153
config/fuzz_test.go Normal file
View File

@@ -0,0 +1,153 @@
package config
import (
"testing"
)
func FuzzParseUniverse(f *testing.F) {
f.Add("artnet:0.0.0")
f.Add("artnet:0.0.1")
f.Add("artnet:127.15.15")
f.Add("artnet:0")
f.Add("artnet:32767")
f.Add("sacn:1")
f.Add("sacn:63999")
f.Add("sacn:100")
f.Add("")
f.Add("invalid")
f.Add("artnet:")
f.Add("sacn:")
f.Add("artnet:a.b.c")
f.Add("artnet:-1")
f.Add("sacn:0")
f.Add("sacn:64000")
f.Fuzz(func(t *testing.T, input string) {
u, err := ParseUniverse(input)
if err != nil {
return
}
s := u.String()
u2, err := ParseUniverse(s)
if err != nil {
t.Fatalf("roundtrip failed: parsed %q -> %v -> %q, but re-parse failed: %v", input, u, s, err)
}
if u != u2 {
t.Fatalf("roundtrip mismatch: %v != %v", u, u2)
}
})
}
func FuzzFromAddrParse(f *testing.F) {
f.Add("artnet:0.0.0")
f.Add("artnet:0.0.1:1-512")
f.Add("artnet:0.0.1:50-100")
f.Add("artnet:0.0.1:1")
f.Add("artnet:0.0.1:512")
f.Add("artnet:0.0.1:1-")
f.Add("sacn:1:100-200")
f.Add("sacn:100")
f.Add("")
f.Add("artnet:0.0.0:0")
f.Add("artnet:0.0.0:513")
f.Add("artnet:0.0.0:-1")
f.Add("artnet:0.0.0:abc")
f.Fuzz(func(t *testing.T, input string) {
var addr FromAddr
err := addr.parse(input)
if err != nil {
return
}
if addr.ChannelStart > addr.ChannelEnd {
t.Fatalf("ChannelStart > ChannelEnd: %d > %d", addr.ChannelStart, addr.ChannelEnd)
}
if addr.ChannelStart < 1 || addr.ChannelEnd > 512 {
return
}
s := addr.String()
var addr2 FromAddr
if err := addr2.parse(s); err != nil {
t.Fatalf("roundtrip failed: parsed %q -> %v -> %q, but re-parse failed: %v", input, addr, s, err)
}
})
}
func FuzzToAddrParse(f *testing.F) {
f.Add("artnet:0.0.0")
f.Add("artnet:0.0.1:1")
f.Add("artnet:0.0.1:512")
f.Add("sacn:1:100")
f.Add("sacn:100")
f.Add("")
f.Add("artnet:0.0.0:0")
f.Add("artnet:0.0.0:1-100")
f.Add("artnet:0.0.0:513")
f.Fuzz(func(t *testing.T, input string) {
var addr ToAddr
err := addr.parse(input)
if err != nil {
return
}
if addr.ChannelStart < 1 || addr.ChannelStart > 512 {
return
}
s := addr.String()
var addr2 ToAddr
if err := addr2.parse(s); err != nil {
t.Fatalf("roundtrip failed: parsed %q -> %v -> %q, but re-parse failed: %v", input, addr, s, err)
}
})
}
func FuzzParseUniverseNumber(f *testing.F) {
f.Add("0.0.0", string(ProtocolArtNet))
f.Add("127.15.15", string(ProtocolArtNet))
f.Add("0", string(ProtocolArtNet))
f.Add("32767", string(ProtocolArtNet))
f.Add("1", string(ProtocolSACN))
f.Add("63999", string(ProtocolSACN))
f.Add("", string(ProtocolArtNet))
f.Add("invalid", string(ProtocolArtNet))
f.Add("0.0", string(ProtocolArtNet))
f.Add("0.0.0.0", string(ProtocolArtNet))
f.Add("-1", string(ProtocolArtNet))
f.Fuzz(func(t *testing.T, input string, protoStr string) {
proto := Protocol(protoStr)
if proto != ProtocolArtNet && proto != ProtocolSACN {
return
}
_, _ = parseUniverseNumber(input, proto)
})
}
func FuzzParseChannelRange(f *testing.F) {
f.Add("1-512")
f.Add("1")
f.Add("512")
f.Add("1-")
f.Add("100-200")
f.Add("")
f.Add("-")
f.Add("abc")
f.Add("1-abc")
f.Add("-1")
f.Add("0")
f.Add("513")
f.Fuzz(func(t *testing.T, input string) {
var start, end int
err := parseChannelRange(input, &start, &end)
if err != nil {
return
}
if start < 0 {
t.Fatalf("start should not be negative: %d", start)
}
if end < 0 {
t.Fatalf("end should not be negative: %d", end)
}
})
}

137
remap/fuzz_test.go Normal file
View File

@@ -0,0 +1,137 @@
package remap
import (
"testing"
"github.com/gopatchy/artmap/config"
)
func FuzzRemap(f *testing.F) {
f.Add(uint16(0), uint16(1), 0, 0, 512, make([]byte, 512))
f.Add(uint16(0), uint16(1), 100, 200, 100, make([]byte, 512))
f.Add(uint16(0), uint16(1), 511, 511, 1, make([]byte, 512))
f.Add(uint16(100), uint16(200), 0, 0, 256, make([]byte, 512))
f.Fuzz(func(t *testing.T, srcUni, dstUni uint16, fromChan, toChan, count int, inputData []byte) {
if fromChan < 0 || fromChan >= 512 {
return
}
if toChan < 0 || toChan >= 512 {
return
}
if count < 1 || count > 512 {
return
}
if fromChan+count > 512 || toChan+count > 512 {
return
}
if len(inputData) < 512 {
return
}
srcU, _ := config.NewUniverse(config.ProtocolArtNet, srcUni)
dstU, _ := config.NewUniverse(config.ProtocolArtNet, dstUni)
mappings := []config.NormalizedMapping{{
From: srcU,
FromChan: fromChan,
To: dstU,
ToChan: toChan,
Count: count,
}}
engine := NewEngine(mappings)
var srcData [512]byte
copy(srcData[:], inputData[:512])
outputs := engine.Remap(srcU, srcData)
if len(outputs) != 1 {
t.Fatalf("expected 1 output, got %d", len(outputs))
}
for i := 0; i < count; i++ {
srcIdx := fromChan + i
dstIdx := toChan + i
if outputs[0].Data[dstIdx] != srcData[srcIdx] {
t.Fatalf("channel mismatch at offset %d: src[%d]=%d != dst[%d]=%d",
i, srcIdx, srcData[srcIdx], dstIdx, outputs[0].Data[dstIdx])
}
}
})
}
func FuzzRemapMultipleMappings(f *testing.F) {
f.Add(make([]byte, 512))
f.Fuzz(func(t *testing.T, inputData []byte) {
if len(inputData) < 512 {
return
}
srcU, _ := config.NewUniverse(config.ProtocolArtNet, 0)
dstU1, _ := config.NewUniverse(config.ProtocolArtNet, 1)
dstU2, _ := config.NewUniverse(config.ProtocolSACN, 1)
mappings := []config.NormalizedMapping{
{From: srcU, FromChan: 0, To: dstU1, ToChan: 0, Count: 256},
{From: srcU, FromChan: 256, To: dstU2, ToChan: 0, Count: 256},
}
engine := NewEngine(mappings)
var srcData [512]byte
copy(srcData[:], inputData[:512])
outputs := engine.Remap(srcU, srcData)
if len(outputs) != 2 {
t.Fatalf("expected 2 outputs, got %d", len(outputs))
}
for _, out := range outputs {
if out.Universe == dstU1 {
for i := 0; i < 256; i++ {
if out.Data[i] != srcData[i] {
t.Fatalf("dstU1 mismatch at %d", i)
}
}
} else if out.Universe == dstU2 {
for i := 0; i < 256; i++ {
if out.Data[i] != srcData[256+i] {
t.Fatalf("dstU2 mismatch at %d", i)
}
}
}
}
})
}
func FuzzRemapUnmatchedUniverse(f *testing.F) {
f.Add(make([]byte, 512))
f.Fuzz(func(t *testing.T, inputData []byte) {
if len(inputData) < 512 {
return
}
srcU, _ := config.NewUniverse(config.ProtocolArtNet, 0)
otherU, _ := config.NewUniverse(config.ProtocolArtNet, 99)
dstU, _ := config.NewUniverse(config.ProtocolArtNet, 1)
mappings := []config.NormalizedMapping{{
From: srcU, FromChan: 0, To: dstU, ToChan: 0, Count: 512,
}}
engine := NewEngine(mappings)
var srcData [512]byte
copy(srcData[:], inputData[:512])
outputs := engine.Remap(otherU, srcData)
if outputs != nil {
t.Fatalf("expected nil output for unmatched universe, got %d outputs", len(outputs))
}
})
}

91
sacn/fuzz_test.go Normal file
View File

@@ -0,0 +1,91 @@
package sacn
import (
"bytes"
"testing"
)
func FuzzParsePacket(f *testing.F) {
cid := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
validPacket := BuildDataPacket(1, 0, "test", cid, make([]byte, 512))
f.Add(validPacket)
f.Add(BuildDataPacket(1, 0, "test", cid, make([]byte, 100)))
f.Add(BuildDataPacket(63999, 255, "long source name here", cid, make([]byte, 512)))
f.Add([]byte{})
f.Add(make([]byte, 125))
f.Add(make([]byte, 126))
f.Add(make([]byte, 638))
f.Fuzz(func(t *testing.T, data []byte) {
_, dmxData, ok := ParsePacket(data)
if !ok {
return
}
if len(dmxData) != 512 {
t.Fatalf("dmx data should be 512 bytes, got %d", len(dmxData))
}
})
}
func FuzzBuildParseRoundtrip(f *testing.F) {
f.Add(uint16(1), uint8(0), "test", make([]byte, 512))
f.Add(uint16(63999), uint8(255), "source", make([]byte, 100))
f.Add(uint16(100), uint8(128), "", make([]byte, 0))
f.Add(uint16(1), uint8(0), "a]very long source name that exceeds normal limits", make([]byte, 512))
f.Fuzz(func(t *testing.T, universe uint16, seq uint8, sourceName string, dmxInput []byte) {
if universe < 1 || universe > 63999 {
return
}
cid := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
packet := BuildDataPacket(universe, seq, sourceName, cid, dmxInput)
parsedUniverse, parsedData, ok := ParsePacket(packet)
if !ok {
t.Fatalf("failed to parse packet we just built")
}
if parsedUniverse != universe {
t.Fatalf("universe mismatch: sent %d, got %d", universe, parsedUniverse)
}
expectedLen := len(dmxInput)
if expectedLen > 512 {
expectedLen = 512
}
if !bytes.Equal(parsedData[:expectedLen], dmxInput[:expectedLen]) {
t.Fatalf("dmx data mismatch")
}
})
}
func ParsePacket(data []byte) (universe uint16, dmxData [512]byte, ok bool) {
if len(data) < 126 {
return 0, dmxData, false
}
if data[4] != 0x41 || data[5] != 0x53 || data[6] != 0x43 {
return 0, dmxData, false
}
rootVector := uint32(data[18])<<24 | uint32(data[19])<<16 | uint32(data[20])<<8 | uint32(data[21])
if rootVector != VectorRootE131Data {
return 0, dmxData, false
}
framingVector := uint32(data[40])<<24 | uint32(data[41])<<16 | uint32(data[42])<<8 | uint32(data[43])
if framingVector != VectorE131DataPacket {
return 0, dmxData, false
}
universe = uint16(data[113])<<8 | uint16(data[114])
if data[117] != VectorDMPSetProperty {
return 0, dmxData, false
}
propCount := uint16(data[123])<<8 | uint16(data[124])
if propCount < 1 {
return 0, dmxData, false
}
dmxLen := int(propCount) - 1
if dmxLen > 512 {
dmxLen = 512
}
if len(data) < 126+dmxLen {
return 0, dmxData, false
}
copy(dmxData[:], data[126:126+dmxLen])
return universe, dmxData, true
}