aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--objects/id_test.go10
-rw-r--r--objects/object_file_test.go12
-rw-r--r--objects/object_tree.go191
-rw-r--r--objects/object_tree_test.go67
4 files changed, 269 insertions, 11 deletions
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)
+ }
+ }
+}