From 7b102a8223b7b27a2d33ae662df1fdde952fee48 Mon Sep 17 00:00:00 2001 From: Laria Carolin Chabowski Date: Tue, 27 Jun 2017 15:34:20 +0200 Subject: Implement Tree objects --- objects/id_test.go | 10 +++ objects/object_file_test.go | 12 +-- objects/object_tree.go | 191 ++++++++++++++++++++++++++++++++++++++++++++ objects/object_tree_test.go | 67 ++++++++++++++++ 4 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 objects/object_tree.go create mode 100644 objects/object_tree_test.go diff --git a/objects/id_test.go b/objects/id_test.go index 361798d..5ca4da9 100644 --- a/objects/id_test.go +++ b/objects/id_test.go @@ -4,6 +4,16 @@ import ( "testing" ) +func genId(b byte) (oid ObjectId) { + oid.Algo = OIdAlgoSHA3_256 + oid.Sum = make([]byte, OIdAlgoSHA3_256.sumLength()) + for i := 0; i < OIdAlgoSHA3_256.sumLength(); i++ { + oid.Sum[i] = b + } + + return +} + func TestParseValidId(t *testing.T) { have, err := ParseObjectId("sha3-256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") if err != nil { diff --git a/objects/object_file_test.go b/objects/object_file_test.go index f1cb584..f0d45c3 100644 --- a/objects/object_file_test.go +++ b/objects/object_file_test.go @@ -22,16 +22,6 @@ var ( "blob=sha3-256:5555555555555555555555555555555555555555555555555555555555555555&size=50\n") ) -func genId(b byte) (oid ObjectId) { - oid.Algo = OIdAlgoSHA3_256 - oid.Sum = make([]byte, OIdAlgoSHA3_256.sumLength()) - for i := 0; i < OIdAlgoSHA3_256.sumLength(); i++ { - oid.Sum[i] = b - } - - return -} - func TestSerializeFile(t *testing.T) { have := testFileObj.Payload() @@ -77,7 +67,7 @@ func TestUnserializeEmptyFile(t *testing.T) { } } -func TestUnserializeFailure(t *testing.T) { +func TestUnserializeFileFailure(t *testing.T) { subtests := []struct{ name, payload string }{ {"missing blob", "size=100\n"}, {"empty blob", "blob=&size=100"}, diff --git a/objects/object_tree.go b/objects/object_tree.go new file mode 100644 index 0000000..07c265c --- /dev/null +++ b/objects/object_tree.go @@ -0,0 +1,191 @@ +package objects + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "sort" +) + +type TreeEntryType string + +const ( + TETFile TreeEntryType = "file" + TETDir TreeEntryType = "dir" + TETSymlink TreeEntryType = "symlink" +) + +var treeEntryTypes = map[TreeEntryType]struct{}{ + TETFile: {}, + TETDir: {}, + TETSymlink: {}, +} + +type TreeEntry interface { + Type() TreeEntryType + equalContent(TreeEntry) bool + toProperties() properties +} + +func compareTreeEntries(a, b TreeEntry) bool { + return a.Type() == b.Type() && a.equalContent(b) +} + +type TreeEntryFile ObjectId + +func (tef TreeEntryFile) Type() TreeEntryType { + return TETFile +} + +func (tef TreeEntryFile) toProperties() properties { + return properties{"ref": ObjectId(tef).String()} +} + +func (a TreeEntryFile) equalContent(_b TreeEntry) bool { + b, ok := _b.(TreeEntryFile) + return ok && ObjectId(b).Equals(ObjectId(a)) +} + +type TreeEntryDir ObjectId + +func (ted TreeEntryDir) Type() TreeEntryType { + return TETDir +} + +func (ted TreeEntryDir) toProperties() properties { + return properties{"ref": ObjectId(ted).String()} +} + +func (a TreeEntryDir) equalContent(_b TreeEntry) bool { + b, ok := _b.(TreeEntryDir) + return ok && ObjectId(b).Equals(ObjectId(a)) +} + +type TreeEntrySymlink string + +func (tes TreeEntrySymlink) Type() TreeEntryType { + return TETSymlink +} + +func (tes TreeEntrySymlink) toProperties() properties { + return properties{"target": string(tes)} +} + +func (a TreeEntrySymlink) equalContent(_b TreeEntry) bool { + b, ok := _b.(TreeEntrySymlink) + return ok && b == a +} + +type Tree map[string]TreeEntry + +func (t Tree) Type() ObjectType { + return OTTree +} + +func (t Tree) Payload() (out []byte) { + lines := []string{} + for name, entry := range t { + props := entry.toProperties() + props["type"] = string(entry.Type()) + props["name"] = name + + line, err := props.MarshalText() + if err != nil { + panic(err) + } + + lines = append(lines, string(line)+"\n") + } + + sort.Strings(lines) + + for _, line := range lines { + out = append(out, line...) + } + + return +} + +func getObjectIdFromProps(p properties, key string) (ObjectId, error) { + raw, ok := p[key] + if !ok { + return ObjectId{}, fmt.Errorf("Missing key: %s", key) + } + + oid, err := ParseObjectId(raw) + return oid, err +} + +func (t Tree) FromPayload(payload []byte) error { + sc := bufio.NewScanner(bytes.NewReader(payload)) + + for sc.Scan() { + line := sc.Bytes() + if len(line) == 0 { + continue + } + + props := make(properties) + if err := props.UnmarshalText(line); err != nil { + return nil + } + + entry_type, ok := props["type"] + if !ok { + return errors.New("Missing property: type") + } + + var entry TreeEntry + switch TreeEntryType(entry_type) { + case TETFile: + ref, err := getObjectIdFromProps(props, "ref") + if err != nil { + return err + } + entry = TreeEntryFile(ref) + case TETDir: + ref, err := getObjectIdFromProps(props, "ref") + if err != nil { + return err + } + entry = TreeEntryDir(ref) + case TETSymlink: + target, ok := props["target"] + if !ok { + return errors.New("Missing key: target") + } + entry = TreeEntrySymlink(target) + default: + // TODO: Or should we just ignore this entry? There might be more types in the future... + return fmt.Errorf("Unknown tree entry type: %s", entry_type) + } + + name, ok := props["name"] + if !ok { + return errors.New("Missing property: name") + } + + t[name] = entry + } + + return sc.Err() +} + +func (a Tree) Equals(b Tree) bool { + for k, va := range a { + vb, ok := b[k] + if !ok || va.Type() != vb.Type() || !va.equalContent(vb) { + return false + } + } + + for k := range b { + _, ok := a[k] + if !ok { + return false + } + } + + return true +} diff --git a/objects/object_tree_test.go b/objects/object_tree_test.go new file mode 100644 index 0000000..c80c2e1 --- /dev/null +++ b/objects/object_tree_test.go @@ -0,0 +1,67 @@ +package objects + +import ( + "bytes" + "testing" +) + +var ( + testTreeObj = Tree{ + "foo": TreeEntryFile(genId(0x11)), + "bar": TreeEntryDir(genId(0x22)), + "baz": TreeEntrySymlink("/föö&bär/💾"), // Test special chars and unicode + "😃": TreeEntryFile(genId(0x33)), + } + + testTreeSerialization = []byte("" + + "name=%f0%9f%98%83&ref=sha3-256:3333333333333333333333333333333333333333333333333333333333333333&type=file\n" + + "name=bar&ref=sha3-256:2222222222222222222222222222222222222222222222222222222222222222&type=dir\n" + + "name=baz&target=%2ff%c3%b6%c3%b6%26b%c3%a4r%2f%f0%9f%92%be&type=symlink\n" + + "name=foo&ref=sha3-256:1111111111111111111111111111111111111111111111111111111111111111&type=file\n") +) + +func TestSerializeTree(t *testing.T) { + have := testTreeObj.Payload() + + if !bytes.Equal(have, testTreeSerialization) { + t.Errorf("Unexpected serialization result: %s", have) + } +} + +func TestSerializeEmptyTree(t *testing.T) { + have := Tree{}.Payload() + + if !bytes.Equal(have, []byte{}) { + t.Errorf("Unexpected serialization result: %s", have) + } +} + +func TestUnserializeTree(t *testing.T) { + have := make(Tree) + if err := have.FromPayload(testTreeSerialization); err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if !have.Equals(testTreeObj) { + t.Errorf("Unexpeced unserialization result: %v", have) + } +} + +func TestUnserializeTreeFailure(t *testing.T) { + subtests := []struct{ name, payload string }{ + {"name missing", "ref=sha3-256:0000000000000000000000000000000000000000000000000000000000000000&type=file\n"}, + {"type missing", "name=foo\n"}, + {"unknown type", "name=baz&type=foobar\n"}, + {"file ref missing", "name=foo&type=file\n"}, + {"dir ref missing", "name=foo&type=dir\n"}, + {"symlink target missing", "name=foo&type=symlink\n"}, + } + + for _, subtest := range subtests { + have := make(Tree) + err := have.FromPayload([]byte(subtest.payload)) + if err == nil { + t.Errorf("Unexpected unserialization success: %v", have) + } + } +} -- cgit v1.2.3-70-g09d2