diff --git a/checksum/checksum.go b/checksum/checksum.go new file mode 100644 index 00000000..19ad500d --- /dev/null +++ b/checksum/checksum.go @@ -0,0 +1,181 @@ +package checksum + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" +) + +// Checksum represents v2-compatible checksum. +type Checksum refs.Checksum + +// Type represents the enumeration +// of checksum types. +type Type uint8 + +const ( + // Unknown is an undefined checksum type. + Unknown Type = iota + + // SHA256 is a SHA256 checksum type. + SHA256 + + // TZ is a Tillich-Zemor checksum type. + TZ +) + +// NewFromV2 wraps v2 Checksum message to Checksum. +// +// Nil refs.Checksum converts to nil. +func NewFromV2(cV2 *refs.Checksum) *Checksum { + return (*Checksum)(cV2) +} + +// New creates and initializes blank Checksum. +// +// Defaults: +// - sum: nil; +// - type: Unknown. +func New() *Checksum { + return NewFromV2(new(refs.Checksum)) +} + +// Type returns checksum type. +func (c *Checksum) Type() Type { + switch (*refs.Checksum)(c).GetType() { + case refs.SHA256: + return SHA256 + case refs.TillichZemor: + return TZ + default: + return Unknown + } +} + +// Sum returns checksum bytes. +func (c *Checksum) Sum() []byte { + return (*refs.Checksum)(c).GetSum() +} + +// SetSHA256 sets checksum to SHA256 hash. +func (c *Checksum) SetSHA256(v [sha256.Size]byte) { + checksum := (*refs.Checksum)(c) + + checksum.SetType(refs.SHA256) + checksum.SetSum(v[:]) +} + +// SetTillichZemor sets checksum to Tillich-Zemor hash. +func (c *Checksum) SetTillichZemor(v [64]byte) { + checksum := (*refs.Checksum)(c) + + checksum.SetType(refs.TillichZemor) + checksum.SetSum(v[:]) +} + +// ToV2 converts Checksum to v2 Checksum message. +// +// Nil Checksum converts to nil. +func (c *Checksum) ToV2() *refs.Checksum { + return (*refs.Checksum)(c) +} + +func Equal(cs1, cs2 *Checksum) bool { + return cs1.Type() == cs2.Type() && bytes.Equal(cs1.Sum(), cs2.Sum()) +} + +// Marshal marshals Checksum into a protobuf binary form. +func (c *Checksum) Marshal() ([]byte, error) { + return (*refs.Checksum)(c).StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of Checksum. +func (c *Checksum) Unmarshal(data []byte) error { + return (*refs.Checksum)(c).Unmarshal(data) +} + +// MarshalJSON encodes Checksum to protobuf JSON format. +func (c *Checksum) MarshalJSON() ([]byte, error) { + return (*refs.Checksum)(c).MarshalJSON() +} + +// UnmarshalJSON decodes Checksum from protobuf JSON format. +func (c *Checksum) UnmarshalJSON(data []byte) error { + return (*refs.Checksum)(c).UnmarshalJSON(data) +} + +func (c *Checksum) String() string { + return hex.EncodeToString((*refs.Checksum)(c).GetSum()) +} + +// Parse parses Checksum from its string representation. +func (c *Checksum) Parse(s string) error { + data, err := hex.DecodeString(s) + if err != nil { + return err + } + + var typ refs.ChecksumType + + switch ln := len(data); ln { + default: + return fmt.Errorf("unsupported checksum length %d", ln) + case sha256.Size: + typ = refs.SHA256 + case 64: + typ = refs.TillichZemor + } + + cV2 := (*refs.Checksum)(c) + cV2.SetType(typ) + cV2.SetSum(data) + + return nil +} + +// String returns string representation of Type. +// +// String mapping: +// * TZ: TZ; +// * SHA256: SHA256; +// * Unknown, default: CHECKSUM_TYPE_UNSPECIFIED. +func (m Type) String() string { + var m2 refs.ChecksumType + + switch m { + default: + m2 = refs.UnknownChecksum + case TZ: + m2 = refs.TillichZemor + case SHA256: + m2 = refs.SHA256 + } + + return m2.String() +} + +// FromString parses Type from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (m *Type) FromString(s string) bool { + var g refs.ChecksumType + + ok := g.FromString(s) + + if ok { + switch g { + default: + *m = Unknown + case refs.TillichZemor: + *m = TZ + case refs.SHA256: + *m = SHA256 + } + } + + return ok +} diff --git a/checksum/checksum_test.go b/checksum/checksum_test.go new file mode 100644 index 00000000..ce5515b5 --- /dev/null +++ b/checksum/checksum_test.go @@ -0,0 +1,175 @@ +package checksum + +import ( + "crypto/rand" + "crypto/sha256" + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/stretchr/testify/require" +) + +func randSHA256(t *testing.T) [sha256.Size]byte { + cSHA256 := [sha256.Size]byte{} + _, err := rand.Read(cSHA256[:]) + require.NoError(t, err) + + return cSHA256 +} + +func TestChecksum(t *testing.T) { + c := New() + + cSHA256 := [sha256.Size]byte{} + _, _ = rand.Read(cSHA256[:]) + + c.SetSHA256(cSHA256) + + require.Equal(t, SHA256, c.Type()) + require.Equal(t, cSHA256[:], c.Sum()) + + cV2 := c.ToV2() + + require.Equal(t, refs.SHA256, cV2.GetType()) + require.Equal(t, cSHA256[:], cV2.GetSum()) + + cTZ := [64]byte{} + _, _ = rand.Read(cSHA256[:]) + + c.SetTillichZemor(cTZ) + + require.Equal(t, TZ, c.Type()) + require.Equal(t, cTZ[:], c.Sum()) + + cV2 = c.ToV2() + + require.Equal(t, refs.TillichZemor, cV2.GetType()) + require.Equal(t, cTZ[:], cV2.GetSum()) +} + +func TestEqualChecksums(t *testing.T) { + require.True(t, Equal(nil, nil)) + + csSHA := [sha256.Size]byte{} + _, _ = rand.Read(csSHA[:]) + + cs1 := New() + cs1.SetSHA256(csSHA) + + cs2 := New() + cs2.SetSHA256(csSHA) + + require.True(t, Equal(cs1, cs2)) + + csSHA[0]++ + cs2.SetSHA256(csSHA) + + require.False(t, Equal(cs1, cs2)) +} + +func TestChecksumEncoding(t *testing.T) { + cs := New() + cs.SetSHA256(randSHA256(t)) + + t.Run("binary", func(t *testing.T) { + data, err := cs.Marshal() + require.NoError(t, err) + + c2 := New() + require.NoError(t, c2.Unmarshal(data)) + + require.Equal(t, cs, c2) + }) + + t.Run("json", func(t *testing.T) { + data, err := cs.MarshalJSON() + require.NoError(t, err) + + cs2 := New() + require.NoError(t, cs2.UnmarshalJSON(data)) + + require.Equal(t, cs, cs2) + }) + + t.Run("string", func(t *testing.T) { + cs2 := New() + + require.NoError(t, cs2.Parse(cs.String())) + + require.Equal(t, cs, cs2) + }) +} + +func TestNewChecksumFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *refs.Checksum + + require.Nil(t, NewFromV2(x)) + }) +} + +func TestChecksum_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *Checksum + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewChecksum(t *testing.T) { + t.Run("default values", func(t *testing.T) { + chs := New() + + // check initial values + require.Equal(t, Unknown, chs.Type()) + require.Nil(t, chs.Sum()) + + // convert to v2 message + chsV2 := chs.ToV2() + + require.Equal(t, refs.UnknownChecksum, chsV2.GetType()) + require.Nil(t, chsV2.GetSum()) + }) +} + +type enumIface interface { + FromString(string) bool + String() string +} + +type enumStringItem struct { + val enumIface + str string +} + +func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) { + for _, item := range items { + require.Equal(t, item.str, item.val.String()) + + s := item.val.String() + + require.True(t, e.FromString(s), s) + + require.EqualValues(t, item.val, e, item.val) + } + + // incorrect strings + for _, str := range []string{ + "some string", + "undefined", + } { + require.False(t, e.FromString(str)) + } +} + +func TestChecksumType_String(t *testing.T) { + toPtr := func(v Type) *Type { + return &v + } + + testEnumStrings(t, new(Type), []enumStringItem{ + {val: toPtr(TZ), str: "TZ"}, + {val: toPtr(SHA256), str: "SHA256"}, + {val: toPtr(Unknown), str: "CHECKSUM_TYPE_UNSPECIFIED"}, + }) +}