From 02f9b17ce88d75a594194109c48112d177125816 Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Wed, 7 Dec 2022 09:08:22 +0100 Subject: [PATCH] Reduce dependencies on stringset (#2450) See #2441 Signed-off-by: Dominik Schulz Signed-off-by: Dominik Schulz --- .gitignore | 2 + go.mod | 1 - go.sum | 3 - internal/action/clone.go | 6 +- internal/backend/storage/fossilfs/status.go | 22 +- internal/set/set.go | 237 ++++++++++++++++++++ internal/set/set_test.go | 209 +++++++++++++++++ 7 files changed, 462 insertions(+), 18 deletions(-) create mode 100644 internal/set/set.go create mode 100644 internal/set/set_test.go diff --git a/.gitignore b/.gitignore index e11e7d39b2..bc9c512bab 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ workdir/ .vscode/ NOTICE.new + +debian/ diff --git a/go.mod b/go.mod index 5c2cbea16e..2e5ba28db2 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/gopasspw/gopass go 1.18 require ( - bitbucket.org/creachadair/stringset v0.0.10 filippo.io/age v1.0.0 github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 github.com/atotto/clipboard v0.1.4 diff --git a/go.sum b/go.sum index 15a817aafb..66ca85e8ae 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,7 @@ -bitbucket.org/creachadair/stringset v0.0.10 h1:DZjkR57sJGMycZNVVbl+26bK7vW1kwLQE6qWICWLteI= -bitbucket.org/creachadair/stringset v0.0.10/go.mod h1:6G0fsnHFxynEdWpihfZf44lnBPpTW6nkrcxITVTBHus= filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc= filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0AE5csawV4YXMNGNQQXvLRps3z2Z59OPO+I= github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= diff --git a/internal/action/clone.go b/internal/action/clone.go index d2ddd8f883..3739f0436f 100644 --- a/internal/action/clone.go +++ b/internal/action/clone.go @@ -7,7 +7,6 @@ import ( "path/filepath" "strings" - "bitbucket.org/creachadair/stringset" "github.com/gopasspw/gopass/internal/action/exit" "github.com/gopasspw/gopass/internal/backend" "github.com/gopasspw/gopass/internal/backend/crypto/age" @@ -15,6 +14,7 @@ import ( "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/cui" "github.com/gopasspw/gopass/internal/out" + "github.com/gopasspw/gopass/internal/set" "github.com/gopasspw/gopass/internal/store/root" "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/debug" @@ -195,7 +195,7 @@ func (s *Action) cloneCheckDecryptionKeys(ctx context.Context, mount string) err debug.Log("We have useable private keys") - recpSet := stringset.New(s.Store.ListRecipients(ctx, mount)...) + recpSet := set.New(s.Store.ListRecipients(ctx, mount)...) ids, err := crypto.ListIdentities(ctx) if err != nil { out.Warningf(ctx, "Failed to check decryption keys: %s", err) @@ -203,7 +203,7 @@ func (s *Action) cloneCheckDecryptionKeys(ctx context.Context, mount string) err return nil } - idSet := stringset.New(ids...) + idSet := set.New(ids...) if idSet.IsSubset(recpSet) { out.Noticef(ctx, "Found valid decryption keys. You can now decrypt your passwords.") diff --git a/internal/backend/storage/fossilfs/status.go b/internal/backend/storage/fossilfs/status.go index 94ef519963..28d0a10c7e 100644 --- a/internal/backend/storage/fossilfs/status.go +++ b/internal/backend/storage/fossilfs/status.go @@ -4,14 +4,14 @@ import ( "context" "strings" - "bitbucket.org/creachadair/stringset" + "github.com/gopasspw/gopass/internal/set" ) type fossilStatus struct { - Extra stringset.Set - Added stringset.Set - Edited stringset.Set - Unchanged stringset.Set + Extra set.Set[string] + Added set.Set[string] + Edited set.Set[string] + Unchanged set.Set[string] } func (f *Fossil) getStatus(ctx context.Context) (fossilStatus, error) { @@ -21,10 +21,10 @@ func (f *Fossil) getStatus(ctx context.Context) (fossilStatus, error) { } s := fossilStatus{ - Extra: stringset.New(), - Added: stringset.New(), - Edited: stringset.New(), - Unchanged: stringset.New(), + Extra: set.New[string](), + Added: set.New[string](), + Edited: set.New[string](), + Unchanged: set.New[string](), } for _, line := range strings.Split(string(stdout), "\n") { op, file, found := strings.Cut(line, " ") @@ -46,10 +46,10 @@ func (f *Fossil) getStatus(ctx context.Context) (fossilStatus, error) { return s, nil } -func (fs *fossilStatus) Untracked() stringset.Set { +func (fs *fossilStatus) Untracked() set.Set[string] { return fs.Extra.Union(fs.Added).Union(fs.Edited) } -func (fs *fossilStatus) Staged() stringset.Set { +func (fs *fossilStatus) Staged() set.Set[string] { return fs.Edited.Union(fs.Added) } diff --git a/internal/set/set.go b/internal/set/set.go new file mode 100644 index 0000000000..6edd3c7eb1 --- /dev/null +++ b/internal/set/set.go @@ -0,0 +1,237 @@ +package set + +import ( + "fmt" + "strings" + + "golang.org/x/exp/constraints" +) + +// Set is a generic set type. +type Set[K constraints.Ordered] map[K]bool + +// New initializes a new Set with the given elements. +func New[K constraints.Ordered](elems ...K) Set[K] { + s := make(map[K]bool, len(elems)) + + for _, e := range elems { + s[e] = true + } + + return s +} + +// String returns a string representation of the set. +func (s Set[K]) String() string { + if s.Empty() { + return "ø" + } + elems := make([]string, len(s)) + for i, e := range s.Elements() { + elems[i] = fmt.Sprintf("%v", e) + } + + return fmt.Sprintf("{%s}", strings.Join(elems, ", ")) +} + +// Elements returns the elements of the set in +// sorted order. +func (s Set[K]) Elements() []K { + return SortedKeys(s) +} + +// Empty returns true if the set is empty. +func (s Set[K]) Empty() bool { + return len(s) == 0 +} + +// Len returns the length of the set. +func (s Set[K]) Len() int { + return len(s) +} + +// Clone creates a copy of the set. +func (s Set[K]) Clone() Set[K] { + c := Set[K]{} + c.Update(s) + + return c +} + +// Update adds all elements from s2 to the set. +func (s *Set[K]) Update(s2 Set[K]) bool { + il := len(*s) + if *s == nil && len(s2) > 0 { + *s = make(Set[K], len(s2)) + } + for k := range s2 { + (*s)[k] = true + } + + return len(*s) != il +} + +// Equals returns true if s and s2 contain +// exactly the same elements. +func (s Set[K]) Equals(s2 Set[K]) bool { + return len(s) == len(s2) && s.IsSubset(s2) +} + +// IsSubset returns true if all elements of s +// are contained in s2. +func (s Set[K]) IsSubset(s2 Set[K]) bool { + if s.Empty() { + return true + } + if len(s) > len(s2) { + return false + } + + for k := range s { + if !s2[k] { + return false + } + } + + return true +} + +// Union returns a new set containing all elements from +// s and s2. +func (s Set[K]) Union(s2 Set[K]) Set[K] { + if s.Empty() { + return s2 + } + if s2.Empty() { + return s + } + + set := make(Set[K]) + for k := range s { + set[k] = true + } + for k := range s2 { + set[k] = true + } + + return set +} + +// Add adds the given elements to the set. +func (s *Set[K]) Add(elems ...K) bool { + il := len(*s) + if *s == nil { + *s = make(Set[K]) + } + for _, k := range elems { + (*s)[k] = true + } + + return len(*s) != il +} + +// Remove deletes the given element from the set. +func (s Set[K]) Remove(s2 Set[K]) bool { + if s.Empty() { + return false + } + il := len(s) + for k := range s2 { + delete(s, k) + } + + return len(s) != il +} + +// Discard deletes the given elements from the set. +func (s Set[K]) Discard(elems ...K) bool { + if s.Empty() { + return false + } + il := len(s) + for _, e := range elems { + delete(s, e) + } + + return len(s) != il +} + +// Map returns a new set by applied the function f +// to all it's elements. +func (s Set[K]) Map(f func(K) K) Set[K] { + out := make(Set[K], len(s)) + for k := range s { + out.Add(f(k)) + } + + return out +} + +// Each applies the function f to all it's elements. +func (s Set[K]) Each(f func(K)) { + for k := range s { + f(k) + } +} + +// Select returns a new set with all the elements for +// that f returns true. +func (s Set[K]) Select(f func(K) bool) Set[K] { + out := make(Set[K], len(s)) + for k := range s { + if f(k) { + out.Add(k) + } + } + + return out +} + +// Partition returns two new sets: the first contains all +// the elements for which f returns true. The seconds the others. +func (s Set[K]) Partition(f func(K) bool) (Set[K], Set[K]) { + yes := make(Set[K], len(s)) + no := make(Set[K], len(s)) + for k := range s { + if f(k) { + yes.Add(k) + + continue + } + + no.Add(k) + } + + return yes, no +} + +// Choose returns the first element for which f returns true. +func (s Set[K]) Choose(f func(K) bool) (K, bool) { + if f == nil { + for k := range s { + return k, true + } + } + for k := range s { + if f(k) { + return k, true + } + } + + var zero K + + return zero, false +} + +// Count returns the number of elements for which f returns true. +func (s Set[K]) Count(f func(K) bool) int { + n := 0 + + for k := range s { + if f(k) { + n++ + } + } + + return n +} diff --git a/internal/set/set_test.go b/internal/set/set_test.go new file mode 100644 index 0000000000..efc5730425 --- /dev/null +++ b/internal/set/set_test.go @@ -0,0 +1,209 @@ +package set + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestString(t *testing.T) { + t.Parallel() + + s1 := New[string]() + assert.Equal(t, "ø", s1.String()) + + s1.Add("a", "b", "c") + assert.Equal(t, "{a, b, c}", s1.String()) + + s2 := New(1, 2, 3, 4) + assert.Equal(t, "{1, 2, 3, 4}", s2.String()) +} + +func TestElements(t *testing.T) { + t.Parallel() + + s1 := New("c", "b", "a") + assert.Equal(t, []string{"a", "b", "c"}, s1.Elements()) +} + +func TestClone(t *testing.T) { + t.Parallel() + + s1 := New("a", "b", "c") + s2 := s1.Clone() + + assert.Equal(t, s1, s2) + + s1.Add("d") + assert.NotEqual(t, s1, s2) +} + +func TestUpdate(t *testing.T) { + t.Parallel() + + s1 := New("a", "b", "c") + s1.Update(New("c", "d", "e")) + + assert.Equal(t, New("a", "b", "c", "d", "e"), s1) + + var s2 Set[string] + s2.Update(s1) + assert.Equal(t, New("a", "b", "c", "d", "e"), s2) +} + +func TestEquals(t *testing.T) { + t.Parallel() + + s1 := New("a", "b", "c") + s2 := s1.Clone() + + assert.True(t, s1.Equals(s2)) + + s1.Add("d") + assert.False(t, s1.Equals(s2)) +} + +func TestIsSubset(t *testing.T) { + t.Parallel() + + s1 := New("a", "b", "b", "c", "d") + s2 := New("c", "d") + + assert.True(t, s2.IsSubset(s1)) + + s3 := New[string]() + assert.True(t, s3.IsSubset(s2)) + assert.False(t, s2.IsSubset(s3)) + + s4 := New("foo") + assert.False(t, s4.IsSubset(s1)) +} + +func TestUnion(t *testing.T) { + t.Parallel() + + s1 := New("a", "b", "b", "c", "d") + s2 := New("c", "d") + s3 := New("foo", "bar") + + us := s1.Union(s2).Union(s3) + + assert.Equal(t, New("a", "b", "c", "d", "foo", "bar"), us) + + s4 := New[string]() + assert.Equal(t, s3, s4.Union(s3)) + + assert.Equal(t, s3, s3.Union(s4)) +} + +func TestAdd(t *testing.T) { + t.Parallel() + + var s1 Set[string] + s1.Add("a") + + assert.Equal(t, New("a"), s1) +} + +func TestRemove(t *testing.T) { + t.Parallel() + + s1 := New("a", "b", "b", "c", "d") + s2 := New("c", "d") + s1.Remove(s2) + + assert.Equal(t, New("a", "b"), s1) + + s3 := New[string]() + assert.False(t, s3.Remove(New("a"))) +} + +func TestDiscard(t *testing.T) { + t.Parallel() + + s1 := New("a", "b", "b", "c", "d") + s1.Discard("c", "d") + + assert.Equal(t, New("a", "b"), s1) + + s3 := New[string]() + assert.False(t, s3.Discard("a")) +} + +func TestMap(t *testing.T) { + t.Parallel() + + s1 := New(1, 2, 3) + s2 := s1.Map(func(i int) int { + return i + 1 + }) + + assert.Equal(t, New(2, 3, 4), s2) +} + +func TestEach(t *testing.T) { + t.Parallel() + + seen := 0 + + s1 := New(1, 2, 3) + s1.Each(func(i int) { + seen++ + }) + assert.Equal(t, s1.Len(), seen) +} + +func TestSelect(t *testing.T) { + t.Parallel() + + s1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + s2 := s1.Select(func(i int) bool { + return i%2 == 0 + }) + + assert.Equal(t, New(2, 4, 6, 8, 10), s2) +} + +func TestPartition(t *testing.T) { + t.Parallel() + + s1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + s2, s3 := s1.Partition(func(i int) bool { + return i%2 == 0 + }) + + assert.Equal(t, New(2, 4, 6, 8, 10), s2) + assert.Equal(t, New(1, 3, 5, 7, 9), s3) +} + +func TestChoose(t *testing.T) { + t.Parallel() + + s1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + v, ok := s1.Choose(func(i int) bool { + return i%3 == 0 && i/3 == 3 + }) + assert.Equal(t, 9, v) + assert.True(t, ok) + + v, ok = s1.Choose(func(i int) bool { + return i > 1024 + }) + assert.Equal(t, 0, v) + assert.False(t, ok) + + s2 := New(1) + v, ok = s2.Choose(nil) + assert.Equal(t, v, 1) + assert.True(t, ok) +} + +func TestCount(t *testing.T) { + t.Parallel() + + s1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + c := s1.Count(func(i int) bool { + return i%3 == 0 + }) + assert.Equal(t, 3, c) +}