package fec import ( "crypto/sha256" "encoding/hex" "math/rand/v2" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // slowMul is carry-less multiplication reduced by gfPoly, used to check the tables. func slowMul(a, b byte) byte { var p int x, y := int(a), int(b) for y > 0 { if y&1 != 0 { p ^= x } x <<= 1 if x&0x100 != 0 { x ^= gfPoly } y >>= 1 } return byte(p) } func TestGF(t *testing.T) { for a := range 256 { for b := range 256 { require.Equal(t, slowMul(byte(a), byte(b)), gfMul(byte(a), byte(b)), "%d*%d", a, b) } if a != 0 { require.Equal(t, byte(1), gfMul(byte(a), gfInv(byte(a))), "inv(%d)", a) } } src := make([]byte, 256) for i := range src { src[i] = byte(i) } for c := range 256 { dst := make([]byte, 256) mulAdd(dst, src, byte(c)) for i := range src { require.Equal(t, gfMul(byte(c), src[i]), dst[i]) } } } func TestLayout(t *testing.T) { for msgLen := 1; msgLen <= 20000; msgLen++ { l, err := NewLayout(msgLen, 1176) require.NoError(t, err) require.Equal(t, (msgLen+1175)/1176, l.K) require.LessOrEqual(t, l.SymbolSize, 1176) require.GreaterOrEqual(t, l.K*l.SymbolSize, msgLen) require.Less(t, l.K*l.SymbolSize-msgLen, l.K, "padding must be less than one byte per symbol") } l, err := NewLayout(65535, 1176) require.NoError(t, err) assert.Equal(t, 56, l.K) _, err = NewLayout(0, 1176) require.ErrorIs(t, err, ErrInvalidLayout) _, err = NewLayout(100, 0) require.ErrorIs(t, err, ErrInvalidLayout) _, err = NewLayout(256, 1) require.ErrorIs(t, err, ErrInvalidLayout) _, err = NewLayout(255, 1) require.NoError(t, err) } func randMsg(r *rand.Rand, n int) []byte { b := make([]byte, n) for i := range b { b[i] = byte(r.Uint32()) } return b } func decodeWith(t *testing.T, enc *Encoder, msgLen, maxSym int, indexes []int) []byte { t.Helper() dec, err := NewDecoder(msgLen, maxSym) require.NoError(t, err) for _, idx := range indexes { sym, err := enc.Symbol(nil, idx) require.NoError(t, err) _, err = dec.Add(idx, sym) require.NoError(t, err) } require.True(t, dec.Ready()) // Decode after a prefix, into capacity holding garbage, which must neither // disturb the prefix nor leak into the message. dst := make([]byte, 3, 3+dec.Layout().K*dec.Layout().SymbolSize) for i := range dst[:cap(dst)] { dst[:cap(dst)][i] = 0xa5 } out, err := dec.Decode(dst) require.NoError(t, err) require.Equal(t, []byte{0xa5, 0xa5, 0xa5}, out[:3]) require.Equal(t, &dst[:1][0], &out[:1][0], "decoded in place") return out[3:] } // TestExhaustiveSubsets decodes every k-subset of the first k+m symbols for // small codes, which checks the MDS property directly. func TestExhaustiveSubsets(t *testing.T) { r := rand.New(rand.NewPCG(1, 2)) const maxSym = 16 for k := 1; k <= 10; k++ { for m := 1; m <= 5; m++ { msgLen := (k-1)*maxSym + 1 + r.IntN(maxSym) msg := randMsg(r, msgLen) enc, err := NewEncoder(msg, maxSym) require.NoError(t, err) require.Equal(t, k, enc.Layout().K) n := k + m for mask := 0; mask < 1< 0 { sym[j] = fill[(i+j)%len(fill)] } } if _, err := dec.Add(int(idx), sym); err != nil { t.Fatal(err) } } out, err := dec.Decode(nil) if err == nil { require.Len(t, out, int(msgLen)) } }) } // BenchmarkRepair and BenchmarkDecode use a v3-sized msg2 (~9.8KB, k=9) with // three repair symbols, decoding with three data symbols missing. func BenchmarkRepair(b *testing.B) { msg := randMsg(rand.New(rand.NewPCG(7, 8)), 9800) enc, err := NewEncoder(msg, 1176) require.NoError(b, err) buf := make([]byte, 0, 1176) b.SetBytes(int64(len(msg))) for b.Loop() { for idx := 9; idx < 12; idx++ { buf, _ = enc.Symbol(buf[:0], idx) } } } func BenchmarkDecode(b *testing.B) { msg := randMsg(rand.New(rand.NewPCG(7, 8)), 9800) enc, err := NewEncoder(msg, 1176) require.NoError(b, err) idx := []int{0, 1, 2, 3, 5, 6, 9, 10, 11} syms := make([][]byte, len(idx)) for i, x := range idx { syms[i], _ = enc.Symbol(nil, x) } b.SetBytes(int64(len(msg))) for b.Loop() { dec, _ := NewDecoder(len(msg), 1176) for i, x := range idx { _, _ = dec.Add(x, syms[i]) } if _, err := dec.Decode(nil); err != nil { b.Fatal(err) } } }