diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go new file mode 100644 index 0000000..e6602a6 --- /dev/null +++ b/internal/testutil/testutil.go @@ -0,0 +1,86 @@ +package testutil + +import "fmt" +import "slices" +import "strings" + +// Snake lets you compare blocks of data where the ordering of certain parts may +// be swapped every which way. It is designed for comparing the encoding of +// maps where the ordering of individual elements is inconsistent. +// +// The snake is divided into sectors, which hold a number of variations. For a +// sector to be satisfied by some data, some ordering of it must match the data +// exactly. for the snake to be satisfied by some data, its sectors must match +// the data in order, but the internal ordering of each sector doesn't matter. +type Snake [] [] []byte +// snake sector variation + +// S returns a new snake. +func S(data ...byte) Snake { + return (Snake { }).Add(data...) +} + +// AddVar returns a new snake with the given sector added on to it. Successive +// calls of this method can be chained together to create a big ass snake. +func (sn Snake) AddVar(sector ...[]byte) Snake { + slice := make(Snake, len(sn) + 1) + copy(slice, sn) + slice[len(slice) - 1] = sector + return slice +} + +// Add is like AddVar, but adds a sector with only one variation, which means it +// does not vary, hence why the method is called that. +func (sn Snake) Add(data ...byte) Snake { + return sn.AddVar(data) +} + +// Check determines if the data satisfies the snake. +func (sn Snake) Check(data []byte) (ok bool, n int) { + left := data + variations := map[int] []byte { } + for _, sector := range sn { + clear(variations) + for key, variation := range sector { + variations[key] = variation + } + for len(variations) > 0 { + found := false + for key, variation := range variations { + if len(left) < len(variation) { continue } + if !slices.Equal(left[:len(variation)], variation) { continue } + n += len(variation) + left = data[n:] + delete(variations, key) + found = true + } + if !found { return false, n } + } + } + return true, n +} + +func (sn Snake) String() string { + out := strings.Builder { } + for index, sector := range sn { + if index > 0 { out.WriteString(" : ") } + out.WriteRune('[') + for index, variation := range sector { + if index > 0 { out.WriteString(" / ") } + for _, byt := range variation { + fmt.Fprintf(&out, "%02x", byt) + } + } + out.WriteRune(']') + } + return out.String() +} + +// HexBytes formats bytes into a hexadecimal string. +func HexBytes(data []byte) string { + out := strings.Builder { } + for _, byt := range data { + fmt.Fprintf(&out, "%02x", byt) + } + return out.String() +} diff --git a/internal/testutil/testutil_test.go b/internal/testutil/testutil_test.go new file mode 100644 index 0000000..e8fa5e0 --- /dev/null +++ b/internal/testutil/testutil_test.go @@ -0,0 +1,64 @@ +package testutil + +import "testing" + +func TestSnakeA(test *testing.T) { + snake := S(1, 6).AddVar( + []byte { 1 }, + []byte { 2 }, + []byte { 3 }, + []byte { 4 }, + []byte { 5 }, + ).Add(9) + + test.Log(snake) + + ok, n := snake.Check([]byte { 1, 6, 1, 2, 3, 4, 5, 9 }) + if !ok { test.Fatal("false negative:", n) } + ok, n = snake.Check([]byte { 1, 6, 5, 4, 3, 2, 1, 9 }) + if !ok { test.Fatal("false negative:", n) } + ok, n = snake.Check([]byte { 1, 6, 3, 1, 4, 2, 5, 9 }) + if !ok { test.Fatal("false negative:", n) } + + ok, n = snake.Check([]byte { 1, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 1, 2, 3, 4, 5, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 0, 2, 3, 4, 5, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 7, 1, 4, 2, 5, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 7, 3, 1, 4, 2, 5, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 7, 3, 1, 4, 2, 5, 9 }) + if ok { test.Fatal("false positive:", n) } +} + +func TestSnakeB(test *testing.T) { + snake := S(1, 6).AddVar( + []byte { 1 }, + []byte { 2 }, + ).Add(9).AddVar( + []byte { 3, 2 }, + []byte { 0 }, + []byte { 1, 1, 2, 3 }, + ) + + test.Log(snake) + + ok, n := snake.Check([]byte { 1, 6, 1, 2, 9, 3, 2, 0, 1, 1, 2, 3}) + if !ok { test.Fatal("false negative:", n) } + ok, n = snake.Check([]byte { 1, 6, 2, 1, 9, 0, 1, 1, 2, 3, 3, 2}) + if !ok { test.Fatal("false negative:", n) } + + ok, n = snake.Check([]byte { 1, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 1, 2, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 9, 3, 2, 0, 1, 1, 2, 3}) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 2, 9, 0, 1, 1, 2, 3, 3, 2}) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 1, 2, 9, 3, 2, 1, 1, 2, 3}) + if ok { test.Fatal("false positive:", n) } +}