gcs: Make error consistent with rest of codebase.

This updates the error handling in the gcs package to be consistent with
the rest of the code base to provide a proper error type and error codes
that can be programmatically detected.

This is part of the ongoing process to cleanup and improve the gcs
module to the quality level required by consensus code for ultimate
inclusion in header commitments.
This commit is contained in:
Dave Collins 2019-08-19 03:58:06 -05:00
parent 2a8856d026
commit 8aa97edada
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
4 changed files with 194 additions and 24 deletions

69
gcs/error.go Normal file
View File

@ -0,0 +1,69 @@
// Copyright (c) 2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package gcs
import (
"fmt"
)
// ErrorCode identifies a kind of error.
type ErrorCode int
// These constants are used to identify a specific RuleError.
const (
// ErrNTooBig signifies that the filter can't handle N items.
ErrNTooBig ErrorCode = iota
// ErrPTooBig signifies that the filter can't handle `1/2**P`
// collision probability.
ErrPTooBig
// ErrMisserialized signifies a filter was misserialized and is missing the
// N and/or P parameters of a serialized filter.
ErrMisserialized
// numErrorCodes is the maximum error code number used in tests.
numErrorCodes
)
// Map of ErrorCode values back to their constant names for pretty printing.
var errorCodeStrings = map[ErrorCode]string{
ErrNTooBig: "ErrNTooBig",
ErrPTooBig: "ErrPTooBig",
ErrMisserialized: "ErrMisserialized",
}
// String returns the ErrorCode as a human-readable name.
func (e ErrorCode) String() string {
if s := errorCodeStrings[e]; s != "" {
return s
}
return fmt.Sprintf("Unknown ErrorCode (%d)", int(e))
}
// Error identifies a filter-related error. The caller can use type assertions
// to access the ErrorCode field to ascertain the specific reason for the
// failure.
type Error struct {
ErrorCode ErrorCode // Describes the kind of error
Description string // Human readable description of the issue
}
// Error satisfies the error interface and prints human-readable errors.
func (e Error) Error() string {
return e.Description
}
// makeError creates an Error given a set of arguments. The error code must
// be one of the error codes provided by this package.
func makeError(c ErrorCode, desc string) Error {
return Error{ErrorCode: c, Description: desc}
}
// IsError returns whether err is an Error with a matching error code.
func IsErrorCode(err error, c ErrorCode) bool {
e, ok := err.(Error)
return ok && e.ErrorCode == c
}

109
gcs/error_test.go Normal file
View File

@ -0,0 +1,109 @@
// Copyright (c) 2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package gcs
import (
"testing"
)
// TestErrorCodeStringer tests the stringized output for the ErrorCode type.
func TestErrorCodeStringer(t *testing.T) {
tests := []struct {
in ErrorCode
want string
}{
{ErrNTooBig, "ErrNTooBig"},
{ErrPTooBig, "ErrPTooBig"},
{ErrMisserialized, "ErrMisserialized"},
{0xffff, "Unknown ErrorCode (65535)"},
}
// Detect additional error codes that don't have the stringer added.
if len(tests)-1 != int(numErrorCodes) {
t.Errorf("It appears an error code was added without adding an " +
"associated stringer test")
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.String()
if result != test.want {
t.Errorf("String #%d\n got: %s want: %s", i, result, test.want)
continue
}
}
}
// TestError tests the error output for the Error type.
func TestError(t *testing.T) {
tests := []struct {
in Error
want string
}{{
Error{Description: "duplicate block"},
"duplicate block",
}, {
Error{Description: "human-readable error"},
"human-readable error",
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("Error #%d\n got: %s want: %s", i, result, test.want)
continue
}
}
}
// TestIsErrorCode ensures IsErrorCode works as intended.
func TestIsErrorCode(t *testing.T) {
tests := []struct {
name string
err error
code ErrorCode
want bool
}{{
name: "ErrNTooBig testing for ErrNTooBig",
err: makeError(ErrNTooBig, ""),
code: ErrNTooBig,
want: true,
}, {
name: "ErrPTooBig testing for ErrPTooBig",
err: makeError(ErrPTooBig, ""),
code: ErrPTooBig,
want: true,
}, {
name: "ErrMisserialized testing for ErrMisserialized",
err: makeError(ErrMisserialized, ""),
code: ErrMisserialized,
want: true,
}, {
name: "ErrNTooBig error testing for ErrPTooBig",
err: makeError(ErrNTooBig, ""),
code: ErrPTooBig,
want: false,
}, {
name: "ErrNTooBig error testing for unknown error code",
err: makeError(ErrNTooBig, ""),
code: 0xffff,
want: false,
}, {
name: "nil error testing for ErrNTooBig",
err: nil,
code: ErrNTooBig,
want: false,
}}
for _, test := range tests {
result := IsErrorCode(test.err, test.code)
if result != test.want {
t.Errorf("%s: unexpected result -- got: %v want: %v", test.name,
result, test.want)
continue
}
}
}

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016-2017 The btcsuite developers
// Copyright (c) 2016-2017 The Lightning Network Developers
// Copyright (c) 2018 The Decred developers
// Copyright (c) 2018-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -8,7 +8,7 @@ package gcs
import (
"encoding/binary"
"errors"
"fmt"
"math"
"sort"
"sync"
@ -20,19 +20,6 @@ import (
// Inspired by https://github.com/rasky/gcs
var (
// ErrNTooBig signifies that the filter can't handle N items.
ErrNTooBig = errors.New("N does not fit in uint32")
// ErrPTooBig signifies that the filter can't handle `1/2**P`
// collision probability.
ErrPTooBig = errors.New("P is too large")
// ErrMisserialized signifies a filter was misserialized and is missing the
// N and/or P parameters of a serialized filter.
ErrMisserialized = errors.New("misserialized filter")
)
// KeySize is the size of the byte array required for key material for the
// SipHash keyed hash function.
const KeySize = 16
@ -66,10 +53,13 @@ func NewFilter(P uint8, key [KeySize]byte, data [][]byte) (*Filter, error) {
// build the filter, and make sure our parameters will fit the hash
// function we're using.
if len(data) > math.MaxInt32 {
return nil, ErrNTooBig
str := fmt.Sprintf("unable to create filter with %d entries greater "+
"than max allowed %d", len(data), math.MaxInt32)
return nil, makeError(ErrNTooBig, str)
}
if P > 32 {
return nil, ErrPTooBig
str := fmt.Sprintf("P value of %d is greater than max allowed 32", P)
return nil, makeError(ErrPTooBig, str)
}
// Create the filter object and insert metadata.
@ -141,7 +131,8 @@ func NewFilter(P uint8, key [KeySize]byte, data [][]byte) (*Filter, error) {
func FromBytes(N uint32, P uint8, d []byte) (*Filter, error) {
// Basic sanity check.
if P > 32 {
return nil, ErrPTooBig
str := fmt.Sprintf("P value of %d is greater than max allowed 32", P)
return nil, makeError(ErrPTooBig, str)
}
// Save the filter data internally as n + filter bytes
@ -165,7 +156,8 @@ func FromNBytes(P uint8, d []byte) (*Filter, error) {
if len(d) >= 4 {
n = binary.BigEndian.Uint32(d[:4])
} else if len(d) < 4 && len(d) != 0 {
return nil, ErrMisserialized
str := "number of items serialization missing"
return nil, makeError(ErrMisserialized, str)
}
f := &Filter{

View File

@ -327,20 +327,20 @@ func TestFilterCorners(t *testing.T) {
const largeP = 33
var key [KeySize]byte
_, err := NewFilter(largeP, key, nil)
if err != ErrPTooBig {
if !IsErrorCode(err, ErrPTooBig) {
t.Fatalf("did not receive expected err for P too big -- got %v, want %v",
err, ErrPTooBig)
}
_, err = FromBytes(0, largeP, nil)
if err != ErrPTooBig {
if !IsErrorCode(err, ErrPTooBig) {
t.Fatalf("did not receive expected err for P too big -- got %v, want %v",
err, ErrPTooBig)
}
// Attempt to decode a filter without the N value serialized properly.
_, err = FromNBytes(20, []byte{0x00})
if err != ErrMisserialized {
t.Fatalf("did not receive expected err -- got %v, want %v",
err, ErrMisserialized)
if !IsErrorCode(err, ErrMisserialized) {
t.Fatalf("did not receive expected err -- got %v, want %v", err,
ErrMisserialized)
}
}