mirror of
https://github.com/luxfi/mlwe.git
synced 2026-10-30 15:24:54 +00:00
206 lines
6.0 KiB
Go
206 lines
6.0 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package codec
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
// TestPackUnpack_RoundTripEveryWidth proves Unpack(Pack(x)) == x for
|
|
// every bit width 1..32 over random coefficients masked to the width.
|
|
func TestPackUnpack_RoundTripEveryWidth(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(1))
|
|
const n = 256
|
|
for bits := 1; bits <= 32; bits++ {
|
|
mask := uint64(1)<<uint(bits) - 1
|
|
in := make([]uint64, n)
|
|
for i := range in {
|
|
in[i] = rng.Uint64() & mask
|
|
}
|
|
packed := PackBits(in, bits)
|
|
if want := (n*bits + 7) / 8; len(packed) != want {
|
|
t.Fatalf("bits=%d: packed len %d, want %d", bits, len(packed), want)
|
|
}
|
|
out := UnpackBits(packed, n, bits)
|
|
for i := range in {
|
|
if out[i] != in[i] {
|
|
t.Fatalf("bits=%d: coeff %d round-trip %d != %d", bits, i, out[i], in[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPackBits_KnownVector pins the LSB-first packing to a hand-computed
|
|
// example so a future refactor can't silently change the wire order.
|
|
func TestPackBits_KnownVector(t *testing.T) {
|
|
// [1,2,3] at 4 bits, LSB-first: 0x21, 0x03.
|
|
got := PackBits([]uint64{1, 2, 3}, 4)
|
|
want := []byte{0x21, 0x03}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("PackBits([1,2,3],4) = % x, want % x", got, want)
|
|
}
|
|
back := UnpackBits(want, 3, 4)
|
|
if back[0] != 1 || back[1] != 2 || back[2] != 3 {
|
|
t.Fatalf("UnpackBits round-trip = %v", back)
|
|
}
|
|
}
|
|
|
|
// TestUnpackBits_ShortDataZeroPads confirms the unpacker is total: a
|
|
// short buffer yields zeros for the missing high bits rather than
|
|
// reading out of bounds or panicking.
|
|
func TestUnpackBits_ShortDataZeroPads(t *testing.T) {
|
|
out := UnpackBits([]byte{0xFF}, 4, 4) // 1 byte = 2 nibbles of data
|
|
want := []uint64{0xF, 0xF, 0, 0}
|
|
for i := range want {
|
|
if out[i] != want[i] {
|
|
t.Fatalf("coeff %d = %d, want %d", i, out[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPackBits_RejectsBadWidth(t *testing.T) {
|
|
for _, bits := range []int{0, -1, 65} {
|
|
func() {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatalf("PackBits(bits=%d) did not panic", bits)
|
|
}
|
|
}()
|
|
PackBits([]uint64{1}, bits)
|
|
}()
|
|
}
|
|
}
|
|
|
|
func TestUnpackBits_RejectsBadWidth(t *testing.T) {
|
|
for _, bits := range []int{0, -1, 65} {
|
|
func() {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatalf("UnpackBits(bits=%d) did not panic", bits)
|
|
}
|
|
}()
|
|
UnpackBits([]byte{0}, 1, bits)
|
|
}()
|
|
}
|
|
}
|
|
|
|
// failWriter fails on the Nth write (1-indexed), to exercise WriteVector
|
|
// error paths.
|
|
type failWriter struct{ failOn, n int }
|
|
|
|
func (f *failWriter) Write(p []byte) (int, error) {
|
|
f.n++
|
|
if f.n >= f.failOn {
|
|
return 0, errors.New("forced write failure")
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func TestWriteVector_LengthPrefixError(t *testing.T) {
|
|
if err := WriteVector(&failWriter{failOn: 1}, []uint64{1, 2}, putU64); err == nil {
|
|
t.Fatalf("expected length-prefix write error")
|
|
}
|
|
}
|
|
|
|
func TestWriteVector_ElementError(t *testing.T) {
|
|
// First write (length prefix) succeeds, the element encode fails.
|
|
if err := WriteVector(&failWriter{failOn: 2}, []uint64{1, 2}, putU64); err == nil {
|
|
t.Fatalf("expected element write error")
|
|
}
|
|
}
|
|
|
|
// putU64 / readU64 are a trivial element codec for ReadVector tests.
|
|
func putU64(w io.Writer, v uint64) error {
|
|
var b [8]byte
|
|
binary.BigEndian.PutUint64(b[:], v)
|
|
_, err := w.Write(b[:])
|
|
return err
|
|
}
|
|
|
|
func getU64(r io.Reader) (uint64, error) {
|
|
var b [8]byte
|
|
if _, err := io.ReadFull(r, b[:]); err != nil {
|
|
return 0, err
|
|
}
|
|
return binary.BigEndian.Uint64(b[:]), nil
|
|
}
|
|
|
|
// TestReadVector_RoundTrip confirms WriteVector/ReadVector are inverse.
|
|
func TestReadVector_RoundTrip(t *testing.T) {
|
|
elems := []uint64{1, 2, 3, 1 << 40, 0}
|
|
var buf bytes.Buffer
|
|
if err := WriteVector(&buf, elems, putU64); err != nil {
|
|
t.Fatalf("WriteVector: %v", err)
|
|
}
|
|
got, err := ReadVector(&buf, 16, getU64)
|
|
if err != nil {
|
|
t.Fatalf("ReadVector: %v", err)
|
|
}
|
|
if len(got) != len(elems) {
|
|
t.Fatalf("len %d != %d", len(got), len(elems))
|
|
}
|
|
for i := range elems {
|
|
if got[i] != elems[i] {
|
|
t.Fatalf("elem %d: %d != %d", i, got[i], elems[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestReadVector_RejectsOversizedFrame is the explicit DoS regression: a
|
|
// frame that declares a 4-billion-element count must be rejected at the
|
|
// length check, BEFORE any per-element allocation, instead of attempting
|
|
// a huge make or unbounded read.
|
|
func TestReadVector_RejectsOversizedFrame(t *testing.T) {
|
|
var frame [4]byte
|
|
binary.BigEndian.PutUint32(frame[:], 0xFFFFFFFF) // ~4.29e9 elements
|
|
// No body bytes at all: a non-bounded reader would try to allocate
|
|
// for 4.29e9 elements first; ours must reject on the length alone.
|
|
got, err := ReadVector(bytes.NewReader(frame[:]), 1024, getU64)
|
|
if !errors.Is(err, ErrFrameTooLarge) {
|
|
t.Fatalf("expected ErrFrameTooLarge, got err=%v got=%v", err, got)
|
|
}
|
|
}
|
|
|
|
// TestReadVector_AtMaxBoundary confirms a count equal to max is accepted
|
|
// and a count of max+1 is rejected.
|
|
func TestReadVector_AtMaxBoundary(t *testing.T) {
|
|
const max = 4
|
|
elems := []uint64{10, 20, 30, 40}
|
|
var ok bytes.Buffer
|
|
_ = WriteVector(&ok, elems, putU64)
|
|
if _, err := ReadVector(&ok, max, getU64); err != nil {
|
|
t.Fatalf("count==max should be accepted: %v", err)
|
|
}
|
|
|
|
var over bytes.Buffer
|
|
_ = WriteVector(&over, append(elems, 50), putU64) // 5 > max
|
|
if _, err := ReadVector(&over, max, getU64); !errors.Is(err, ErrFrameTooLarge) {
|
|
t.Fatalf("count==max+1 should be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestReadVector_TruncatedBody surfaces a short read inside an element.
|
|
func TestReadVector_TruncatedBody(t *testing.T) {
|
|
var frame []byte
|
|
var lp [4]byte
|
|
binary.BigEndian.PutUint32(lp[:], 3) // declares 3 elements
|
|
frame = append(frame, lp[:]...)
|
|
frame = append(frame, 0x00, 0x01) // only 2 bytes of body (need 24)
|
|
if _, err := ReadVector(bytes.NewReader(frame), 8, getU64); err == nil {
|
|
t.Fatalf("expected error on truncated body")
|
|
}
|
|
}
|
|
|
|
// TestReadVector_TruncatedLengthPrefix surfaces a short length read.
|
|
func TestReadVector_TruncatedLengthPrefix(t *testing.T) {
|
|
if _, err := ReadVector(bytes.NewReader([]byte{0x00, 0x01}), 8, getU64); err == nil {
|
|
t.Fatalf("expected error on truncated length prefix")
|
|
}
|
|
}
|