diff --git a/entry.go b/entry.go index 4ed5037..28465e5 100644 --- a/entry.go +++ b/entry.go @@ -52,6 +52,29 @@ func (e *Entry) setKdbxFormatVersion(version formatVersion) { } } +// Clone creates a copy of an Entry struct including its child entities +func (e Entry) Clone() Entry { + clone := e + clone.UUID = NewUUID() + clone.Values = make([]ValueData, len(clone.Values)) + for i, value := range e.Values { + clone.Values[i] = value + } + clone.Histories = make([]History, len(clone.Histories)) + for i, history := range e.Histories { + clone.Histories[i] = history.Clone() + } + clone.Binaries = make([]BinaryReference, len(clone.Binaries)) + for i, binary := range e.Binaries { + clone.Binaries[i] = binary + } + clone.CustomData = make([]CustomData, len(clone.CustomData)) + for i, data := range e.CustomData { + clone.CustomData[i] = data + } + return clone +} + // Get returns the value in e corresponding with key k, or an empty string otherwise func (e *Entry) Get(key string) *ValueData { for i := range e.Values { @@ -108,6 +131,18 @@ func (h *History) setKdbxFormatVersion(version formatVersion) { } } +// Clone creates a copy of a History struct including its child entities +func (h History) Clone() History { + clone := h + + clone.Entries = make([]Entry, len(h.Entries)) + for i, entry := range h.Entries { + clone.Entries[i] = entry.Clone() + } + + return clone +} + // ValueData is a structure containing key value pairs of information stored in an entry type ValueData struct { Key string `xml:"Key"` diff --git a/entry_test.go b/entry_test.go index 1e022fc..3d8c139 100644 --- a/entry_test.go +++ b/entry_test.go @@ -3,6 +3,10 @@ package gokeepasslib import ( "reflect" "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "golang.org/x/exp/slices" ) func TestNewEntry(t *testing.T) { @@ -62,6 +66,94 @@ func TestNewEntry(t *testing.T) { } } +func compareEntry(a, b Entry) bool { + if !cmp.Equal( + a, + b, + cmpopts.IgnoreFields(Entry{}, "Values", "Histories", "Binaries", "CustomData"), + ) { + return false + } + + if !slices.EqualFunc( + a.Values, + b.Values, + func(a, b ValueData) bool { + return cmp.Equal(a, b) + }, + ) { + return false + } + + if !slices.EqualFunc( + a.Histories, + b.Histories, + func(a, b History) bool { + return cmp.Equal(a, b) + }, + ) { + return false + } + + if !slices.EqualFunc( + a.Binaries, + b.Binaries, + func(a, b BinaryReference) bool { + return cmp.Equal(a, b) + }, + ) { + return false + } + + if !slices.EqualFunc( + a.CustomData, + b.CustomData, + func(a, b CustomData) bool { + return cmp.Equal(a, b) + }, + ) { + return false + } + + return true +} + +func TestEntry_Clone(t *testing.T) { + cases := []struct { + title string + }{ + { + title: "success", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + entry := NewEntry() + + clone := entry.Clone() + + if &clone == &entry { + t.Errorf("clone struct has the same pointer address") + } + + if clone.UUID == entry.UUID { + t.Errorf("clone did not receive a new UUID") + } + + clone.UUID = entry.UUID + if !compareEntry(entry, clone) { + t.Errorf( + "Did not receive expected Entry %+v, received %+v", + clone, + entry, + ) + } + + }) + } +} + func TestEntrySetKdbxFormatVersion(t *testing.T) { cases := []struct { title string @@ -127,3 +219,48 @@ func TestEntrySetKdbxFormatVersion(t *testing.T) { } } + +func TestHistory_Clone(t *testing.T) { + cases := []struct { + title string + }{ + { + title: "success", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + entry := NewEntry() + history := History{ + Entries: []Entry{entry}, + } + + clone := history.Clone() + + if &clone == &history { + t.Errorf("clone struct has the same pointer address") + } + + if clone.Entries[0].UUID == history.Entries[0].UUID { + t.Errorf("cloned entry did not receive a new UUID") + } + + clone.Entries[0].UUID = history.Entries[0].UUID + if !slices.EqualFunc( + history.Entries, + clone.Entries, + func(a, b Entry) bool { + return compareEntry(a, b) + }, + ) { + t.Errorf( + "Did not receive expected History %+v, received %+v", + clone, + history, + ) + } + + }) + } +} diff --git a/group.go b/group.go index 6de89ce..c903209 100644 --- a/group.go +++ b/group.go @@ -47,6 +47,21 @@ type Group struct { groupChildOrder int `xml:"-"` } +// Clone creates a copy of a Group struct including its child entities +func (g Group) Clone() Group { + clone := g + clone.UUID = NewUUID() + clone.Entries = make([]Entry, len(clone.Entries)) + for i, entry := range g.Entries { + clone.Entries[i] = entry.Clone() + } + clone.Groups = make([]Group, len(clone.Groups)) + for i, group := range g.Groups { + clone.Groups[i] = group.Clone() + } + return clone +} + // UnmarshalXML unmarshals the boolean from d func (g *Group) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { diff --git a/group_test.go b/group_test.go index 1623d1e..5eab6b7 100644 --- a/group_test.go +++ b/group_test.go @@ -7,7 +7,10 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" w "github.com/tobischo/gokeepasslib/v3/wrappers" ) @@ -65,6 +68,71 @@ func TestNewGroup(t *testing.T) { } } +func compareGroup(a, b Group) bool { + if !cmp.Equal( + a, + b, + cmp.AllowUnexported(Group{}), + cmpopts.IgnoreFields(Group{}, "Entries", "Groups"), + ) { + return false + } + + if !slices.EqualFunc( + a.Groups, + b.Groups, + compareGroup, + ) { + return false + } + + if !slices.EqualFunc( + a.Entries, + b.Entries, + compareEntry, + ) { + return false + } + + return true +} + +func TestGroup_Clone(t *testing.T) { + cases := []struct { + title string + }{ + { + title: "success", + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + group := NewGroup() + + clone := group.Clone() + + if &clone == &group { + t.Errorf("clone struct has the same pointer address") + } + + if clone.UUID == group.UUID { + t.Errorf("clone did not receive a new UUID") + } + + clone.UUID = group.UUID + if !compareGroup(clone, group) { + t.Errorf( + "Did not receive expected Group %+v, received %+v", + clone, + group, + ) + } + + }) + } +} + func TestGroupSetKdbxFormatVersion(t *testing.T) { cases := []struct { title string