diff --git a/gcs/gcs.go b/gcs/gcs.go index b5c4fb8..ccf73e3 100644 --- a/gcs/gcs.go +++ b/gcs/gcs.go @@ -6,6 +6,7 @@ package gcs import ( + "encoding/binary" "fmt" "io" "sort" @@ -140,6 +141,24 @@ func FromBytes(N uint32, P uint8, d []byte) (*Filter, error) { return f, nil } +// FromNBytes deserializes a GCS filter from a known P, and serialized N and +// filter as returned by NBytes(). +func FromNBytes(P uint8, d []byte) (*Filter, error) { + return FromBytes(binary.BigEndian.Uint32(d[:4]), P, d[4:]) +} + +// FromPBytes deserializes a GCS filter from a known N, and serialized P and +// filter as returned by NBytes(). +func FromPBytes(N uint32, d []byte) (*Filter, error) { + return FromBytes(N, d[0], d[1:]) +} + +// FromNPBytes deserializes a GCS filter from a serialized N, P, and filter as +// returned by NPBytes(). +func FromNPBytes(d []byte) (*Filter, error) { + return FromBytes(binary.BigEndian.Uint32(d[:4]), d[4], d[5:]) +} + // Bytes returns the serialized format of the GCS filter, which does not // include N or P (returned by separate methods) or the key used by SipHash. func (f *Filter) Bytes() []byte { @@ -148,6 +167,34 @@ func (f *Filter) Bytes() []byte { return filterData } +// NBytes returns the serialized format of the GCS filter with N, which does +// not include P (returned by a separate method) or the key used by SipHash. +func (f *Filter) NBytes() []byte { + filterData := make([]byte, len(f.filterData)+4) + binary.BigEndian.PutUint32(filterData[:4], f.n) + copy(filterData[4:], f.filterData) + return filterData +} + +// PBytes returns the serialized format of the GCS filter with P, which does +// not include N (returned by a separate method) or the key used by SipHash. +func (f *Filter) PBytes() []byte { + filterData := make([]byte, len(f.filterData)+1) + filterData[0] = f.p + copy(filterData[1:], f.filterData) + return filterData +} + +// NPBytes returns the serialized format of the GCS filter with N and P, which +// does not include the key used by SipHash. +func (f *Filter) NPBytes() []byte { + filterData := make([]byte, len(f.filterData)+5) + binary.BigEndian.PutUint32(filterData[:4], f.n) + filterData[4] = f.p + copy(filterData[5:], f.filterData) + return filterData +} + // P returns the filter's collision probability as a negative power of 2 (that // is, a collision probability of `1/2**20` is represented as 20). func (f *Filter) P() uint8 { diff --git a/gcs/gcs_test.go b/gcs/gcs_test.go index 56d8568..de31bd8 100644 --- a/gcs/gcs_test.go +++ b/gcs/gcs_test.go @@ -6,6 +6,7 @@ package gcs_test import ( + "bytes" "encoding/binary" "math/rand" "testing" @@ -23,7 +24,7 @@ var ( // Filters are conserved between tests but we must define with an // interface which functions we're testing because the gcsFilter // type isn't exported - filter, filter2 *gcs.Filter + filter, filter2, filter3, filter4, filter5 *gcs.Filter // We need to use the same key for building and querying the filters key [gcs.KeySize]byte @@ -90,6 +91,18 @@ func TestGCSFilterCopy(t *testing.T) { if err != nil { t.Fatalf("Filter copy failed: %s", err.Error()) } + filter3, err = gcs.FromNBytes(filter.P(), filter.NBytes()) + if err != nil { + t.Fatalf("Filter copy failed: %s", err.Error()) + } + filter4, err = gcs.FromPBytes(filter.N(), filter.PBytes()) + if err != nil { + t.Fatalf("Filter copy failed: %s", err.Error()) + } + filter5, err = gcs.FromNPBytes(filter.NPBytes()) + if err != nil { + t.Fatalf("Filter copy failed: %s", err.Error()) + } } // TestGCSFilterMetadata checks that the filter metadata is built and @@ -104,9 +117,39 @@ func TestGCSFilterMetadata(t *testing.T) { if filter.P() != filter2.P() { t.Fatal("P doesn't match between copied filters") } + if filter.P() != filter3.P() { + t.Fatal("P doesn't match between copied filters") + } + if filter.P() != filter4.P() { + t.Fatal("P doesn't match between copied filters") + } + if filter.P() != filter5.P() { + t.Fatal("P doesn't match between copied filters") + } if filter.N() != filter2.N() { t.Fatal("N doesn't match between copied filters") } + if filter.N() != filter3.N() { + t.Fatal("N doesn't match between copied filters") + } + if filter.N() != filter4.N() { + t.Fatal("N doesn't match between copied filters") + } + if filter.N() != filter5.N() { + t.Fatal("N doesn't match between copied filters") + } + if !bytes.Equal(filter.Bytes(), filter2.Bytes()) { + t.Fatal("Bytes don't match between copied filters") + } + if !bytes.Equal(filter.Bytes(), filter3.Bytes()) { + t.Fatal("Bytes don't match between copied filters") + } + if !bytes.Equal(filter.Bytes(), filter4.Bytes()) { + t.Fatal("Bytes don't match between copied filters") + } + if !bytes.Equal(filter.Bytes(), filter5.Bytes()) { + t.Fatal("Bytes don't match between copied filters") + } } // TestGCSFilterMatch checks that both the built and copied filters match