diff --git a/CHANGELOG.md b/CHANGELOG.md index a9509d5d3..b85072e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,20 @@ # Changelog -## 0.8.2 (April 12th, 2018) +## 0.8.2 (April 23rd, 2018) FEATURES: + - [pubsub] TagMap, NewTagMap + - [merkle] SimpleProofsFromMap() + - [common] IsASCIIText() + - [common] PrefixEndBytes // e.g. increment or nil + - [common] BitArray.MarshalJSON/.UnmarshalJSON + - [common] BitArray uses 'x' not 'X' for String() and above. - [db] DebugDB shows better colorized output BUG FIXES: + - [common] Fix TestParallelAbort nondeterministic failure #201/#202 - [db] PrefixDB Iterator/ReverseIterator fixes - [db] DebugDB fixes diff --git a/common/bit_array.go b/common/bit_array.go index ea6a6ee1f..0290921a6 100644 --- a/common/bit_array.go +++ b/common/bit_array.go @@ -3,6 +3,7 @@ package common import ( "encoding/binary" "fmt" + "regexp" "strings" "sync" ) @@ -249,13 +250,14 @@ func (bA *BitArray) PickRandom() (int, bool) { return 0, false } +// String returns a string representation of BitArray: BA{}, +// where is a sequence of 'x' (1) and '_' (0). +// The includes spaces and newlines to help people. +// For a simple sequence of 'x' and '_' characters with no spaces or newlines, +// see the MarshalJSON() method. +// Example: "BA{_x_}" or "nil-BitArray" for nil. func (bA *BitArray) String() string { - if bA == nil { - return "nil-BitArray" - } - bA.mtx.Lock() - defer bA.mtx.Unlock() - return bA.stringIndented("") + return bA.StringIndented("") } func (bA *BitArray) StringIndented(indent string) string { @@ -268,12 +270,11 @@ func (bA *BitArray) StringIndented(indent string) string { } func (bA *BitArray) stringIndented(indent string) string { - lines := []string{} bits := "" for i := 0; i < bA.Bits; i++ { if bA.getIndex(i) { - bits += "X" + bits += "x" } else { bits += "_" } @@ -282,10 +283,10 @@ func (bA *BitArray) stringIndented(indent string) string { bits = "" } if i%10 == 9 { - bits += " " + bits += indent } if i%50 == 49 { - bits += " " + bits += indent } } if len(bits) > 0 { @@ -320,3 +321,58 @@ func (bA *BitArray) Update(o *BitArray) { copy(bA.Elems, o.Elems) } + +// MarshalJSON implements json.Marshaler interface by marshaling bit array +// using a custom format: a string of '-' or 'x' where 'x' denotes the 1 bit. +func (bA *BitArray) MarshalJSON() ([]byte, error) { + if bA == nil { + return []byte("null"), nil + } + + bA.mtx.Lock() + defer bA.mtx.Unlock() + + bits := `"` + for i := 0; i < bA.Bits; i++ { + if bA.getIndex(i) { + bits += `x` + } else { + bits += `_` + } + } + bits += `"` + return []byte(bits), nil +} + +var bitArrayJSONRegexp = regexp.MustCompile(`\A"([_x]*)"\z`) + +// UnmarshalJSON implements json.Unmarshaler interface by unmarshaling a custom +// JSON description. +func (bA *BitArray) UnmarshalJSON(bz []byte) error { + b := string(bz) + if b == "null" { + // This is required e.g. for encoding/json when decoding + // into a pointer with pre-allocated BitArray. + bA.Bits = 0 + bA.Elems = nil + return nil + } + + // Validate 'b'. + match := bitArrayJSONRegexp.FindStringSubmatch(b) + if match == nil { + return fmt.Errorf("BitArray in JSON should be a string of format %q but got %s", bitArrayJSONRegexp.String(), b) + } + bits := match[1] + + // Construct new BitArray and copy over. + numBits := len(bits) + bA2 := NewBitArray(numBits) + for i := 0; i < numBits; i++ { + if bits[i] == 'x' { + bA2.SetIndex(i, true) + } + } + *bA = *bA2 + return nil +} diff --git a/common/bit_array_test.go b/common/bit_array_test.go index fbc438cd1..c697ba5de 100644 --- a/common/bit_array_test.go +++ b/common/bit_array_test.go @@ -2,8 +2,10 @@ package common import ( "bytes" + "encoding/json" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -210,8 +212,56 @@ func TestUpdateNeverPanics(t *testing.T) { } func TestNewBitArrayNeverCrashesOnNegatives(t *testing.T) { - bitList := []int{-127, -128, -1<<31} + bitList := []int{-127, -128, -1 << 31} for _, bits := range bitList { _ = NewBitArray(bits) } } + +func TestJSONMarshalUnmarshal(t *testing.T) { + + bA1 := NewBitArray(0) + + bA2 := NewBitArray(1) + + bA3 := NewBitArray(1) + bA3.SetIndex(0, true) + + bA4 := NewBitArray(5) + bA4.SetIndex(0, true) + bA4.SetIndex(1, true) + + testCases := []struct { + bA *BitArray + marshalledBA string + }{ + {nil, `null`}, + {bA1, `null`}, + {bA2, `"_"`}, + {bA3, `"x"`}, + {bA4, `"xx___"`}, + } + + for _, tc := range testCases { + t.Run(tc.bA.String(), func(t *testing.T) { + bz, err := json.Marshal(tc.bA) + require.NoError(t, err) + + assert.Equal(t, tc.marshalledBA, string(bz)) + + var unmarshalledBA *BitArray + err = json.Unmarshal(bz, &unmarshalledBA) + require.NoError(t, err) + + if tc.bA == nil { + require.Nil(t, unmarshalledBA) + } else { + require.NotNil(t, unmarshalledBA) + assert.EqualValues(t, tc.bA.Bits, unmarshalledBA.Bits) + if assert.EqualValues(t, tc.bA.String(), unmarshalledBA.String()) { + assert.EqualValues(t, tc.bA.Elems, unmarshalledBA.Elems) + } + } + }) + } +} diff --git a/common/string.go b/common/string.go index 0e2231e91..ccfa0cd3a 100644 --- a/common/string.go +++ b/common/string.go @@ -57,3 +57,18 @@ func SplitAndTrim(s, sep, cutset string) []string { } return spl } + +// Returns true if s is a non-empty printable non-tab ascii character. +func IsASCIIText(s string) bool { + if len(s) == 0 { + return false + } + for _, b := range []byte(s) { + if 32 <= b && b <= 126 { + // good + } else { + return false + } + } + return true +} diff --git a/common/string_test.go b/common/string_test.go index 82ba67844..fecf1dab7 100644 --- a/common/string_test.go +++ b/common/string_test.go @@ -49,3 +49,18 @@ func TestSplitAndTrim(t *testing.T) { assert.Equal(t, tc.expected, SplitAndTrim(tc.s, tc.sep, tc.cutset), "%s", tc.s) } } + +func TestIsASCIIText(t *testing.T) { + notASCIIText := []string{ + "", "\xC2", "\xC2\xA2", "\xFF", "\x80", "\xF0", "\n", "\t", + } + for _, v := range notASCIIText { + assert.False(t, IsHex(v), "%q is not ascii-text", v) + } + asciiText := []string{ + " ", ".", "x", "$", "_", "abcdefg;", "-", "0x00", "0", "123", + } + for _, v := range asciiText { + assert.True(t, IsASCIIText(v), "%q is ascii-text", v) + } +}