From fb14fb157c0835d61cbc7f8425d89b941bd0ab16 Mon Sep 17 00:00:00 2001 From: Laria Carolin Chabowski Date: Sun, 25 Jun 2017 10:20:26 +0200 Subject: Implement object IDs --- objects/id.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ objects/id_test.go | 77 ++++++++++++++++++++++++++++++++++ objects/object.go | 17 ++++++-- objects/object_test.go | 18 ++++++++ 4 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 objects/id.go create mode 100644 objects/id_test.go diff --git a/objects/id.go b/objects/id.go new file mode 100644 index 0000000..22eeaa2 --- /dev/null +++ b/objects/id.go @@ -0,0 +1,111 @@ +package objects + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "golang.org/x/crypto/sha3" + "hash" + "io" + "strings" +) + +type ObjectIdAlgo string + +const ( + OIdAlgoSHA3_256 ObjectIdAlgo = "sha3-256" + OIdAlgoDefault = OIdAlgoSHA3_256 +) + +var allowedAlgos = map[ObjectIdAlgo]struct{}{OIdAlgoSHA3_256: {}} + +func (algo ObjectIdAlgo) checkAlgo() bool { + _, ok := allowedAlgos[algo] + return ok +} + +func (algo ObjectIdAlgo) sumLength() int { + if algo != OIdAlgoSHA3_256 { + panic("Only sha3-256 is implemented!") + } + + return 32 +} + +// ObjectId identifies an object using a cryptogrpahic hash +type ObjectId struct { + Algo ObjectIdAlgo + Sum []byte +} + +func (oid ObjectId) wellformed() bool { + return oid.Algo.checkAlgo() && len(oid.Sum) == oid.Algo.sumLength() +} + +func (oid ObjectId) String() string { + return fmt.Sprintf("%s:%s", oid.Algo, hex.EncodeToString(oid.Sum)) +} + +func ParseObjectId(s string) (oid ObjectId, err error) { + parts := strings.SplitN(s, ":", 2) + if len(parts) != 2 { + err = errors.New("Invalid ObjectId format") + return + } + + oid.Algo = ObjectIdAlgo(parts[0]) + + oid.Sum, err = hex.DecodeString(parts[1]) + if err != nil { + return + } + + if !oid.wellformed() { + err = errors.New("Object ID is malformed") + } + + return +} + +func (a ObjectId) Equals(b ObjectId) bool { + return a.Algo == b.Algo && bytes.Equal(a.Sum, b.Sum) +} + +// ObjectIdGenerator generates an ObjectId from the binary representation of an object +type ObjectIdGenerator interface { + io.Writer // Must not fail + GetId() ObjectId +} + +func (algo ObjectIdAlgo) Generator() ObjectIdGenerator { + if algo != OIdAlgoSHA3_256 { + panic("Only sha3-256 is implemented!") + } + + return hashObjectIdGenerator{ + algo: algo, + Hash: sha3.New256(), + } +} + +type hashObjectIdGenerator struct { + algo ObjectIdAlgo + hash.Hash +} + +func (h hashObjectIdGenerator) GetId() ObjectId { + return ObjectId{ + Algo: h.algo, + Sum: h.Sum([]byte{}), + } +} + +func (oid ObjectId) VerifyObject(o Object) bool { + gen := oid.Algo.Generator() + if err := o.Serialize(gen); err != nil { + panic(err) + } + + return gen.GetId().Equals(oid) +} diff --git a/objects/id_test.go b/objects/id_test.go new file mode 100644 index 0000000..361798d --- /dev/null +++ b/objects/id_test.go @@ -0,0 +1,77 @@ +package objects + +import ( + "testing" +) + +func TestParseValidId(t *testing.T) { + have, err := ParseObjectId("sha3-256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + want := ObjectId{ + Algo: OIdAlgoSHA3_256, + Sum: []byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }, + } + + if !have.Equals(want) { + t.Errorf("unexpected result, want: '%s', have: '%s'", want, have) + } +} + +func TestParseMalformedIds(t *testing.T) { + malformed := []string{ + "", // Empty not permitted + "sha3-256", // Missing : + "sha3-256:", // Missing hex sum + ":abcdef", // Missing algo + "foobar:abcdef", // Basic format ok, but unknown algo + "sha3-256:foobar", // Not hexadecimal + "sha3-256:abcdef", // sum length mismatch + } + + for _, s := range malformed { + oid, err := ParseObjectId(s) + if err == nil { + t.Errorf("'%s' resulted in valid id (%s), expected error", s, oid) + } + } +} + +func TestParseSerialize(t *testing.T) { + want := ObjectId{ + Algo: OIdAlgoSHA3_256, + Sum: []byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }, + } + + have, err := ParseObjectId(want.String()) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if !have.Equals(want) { + t.Errorf("unexpected result, want: '%s', have: '%s'", want, have) + } +} + +func TestSerializeParse(t *testing.T) { + want := "sha3-256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + + oid, err := ParseObjectId(want) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + have := oid.String() + + if have != want { + t.Errorf("unexpected result, want: '%s', have: '%s'", want, have) + } +} diff --git a/objects/object.go b/objects/object.go index 2bc3a53..a1d2335 100644 --- a/objects/object.go +++ b/objects/object.go @@ -7,10 +7,6 @@ import ( "strings" ) -type ObjectId interface { - fmt.Stringer -} - type ObjectType string const ( @@ -25,6 +21,7 @@ type Object struct { Payload []byte } +// SerialiteObject writes the binary representation of an object to a io.Writer func (o Object) Serialize(w io.Writer) error { if _, err := fmt.Fprintf(w, "%s %d\n", o.Type, len(o.Payload)); err != nil { return err @@ -34,6 +31,16 @@ func (o Object) Serialize(w io.Writer) error { return err } +func (o Object) SerializeAndId(w io.Writer, algo ObjectIdAlgo) (ObjectId, error) { + gen := algo.Generator() + + if err := o.Serialize(io.MultiWriter(w, gen)); err != nil { + return ObjectId{}, err + } + + return gen.GetId(), nil +} + type UnserializeError struct { Reason error } @@ -65,6 +72,8 @@ func (br *bytewiseReader) ReadByte() (b byte, err error) { return } +// UnserializeObject attempts to read an object from a stream. +// It is advisable to pass a buffered reader, if feasible. func UnserializeObject(r io.Reader) (Object, error) { br := newBytewiseReader(r) diff --git a/objects/object_test.go b/objects/object_test.go index c596b39..54b33db 100644 --- a/objects/object_test.go +++ b/objects/object_test.go @@ -86,3 +86,21 @@ func TestSerialize(t *testing.T) { t.Errorf("Unexpected serialization result: %v", b) } } + +func TestSerializeAndId(t *testing.T) { + o := Object{ + Type: OTBlob, + Payload: []byte("foo bar\nbaz"), + } + + buf := new(bytes.Buffer) + id, err := o.SerializeAndId(buf, OIdAlgoDefault) + + if err != nil { + t.Fatalf("Serialization failed:%s", err) + } + + if !id.VerifyObject(o) { + t.Errorf("Verification failed") + } +} -- cgit v1.2.3-54-g00ecf