From 416e67b3f127eb356e1fc9d758121f7c8e97e61f Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 28 Jan 2026 12:34:32 -0800 Subject: [PATCH] Add fuzz tests for config, remap, and sacn packages --- config/fuzz_test.go | 153 ++++++++++++++++++++++++++++++++++++++++++++ remap/fuzz_test.go | 137 +++++++++++++++++++++++++++++++++++++++ sacn/fuzz_test.go | 91 ++++++++++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 config/fuzz_test.go create mode 100644 remap/fuzz_test.go create mode 100644 sacn/fuzz_test.go diff --git a/config/fuzz_test.go b/config/fuzz_test.go new file mode 100644 index 0000000..8c14110 --- /dev/null +++ b/config/fuzz_test.go @@ -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) + } + }) +} diff --git a/remap/fuzz_test.go b/remap/fuzz_test.go new file mode 100644 index 0000000..51aaec3 --- /dev/null +++ b/remap/fuzz_test.go @@ -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)) + } + }) +} diff --git a/sacn/fuzz_test.go b/sacn/fuzz_test.go new file mode 100644 index 0000000..7f783ac --- /dev/null +++ b/sacn/fuzz_test.go @@ -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 +}