lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 2c9d49c266c5387ea1d1eef0b8270135b0ca341e
parent b8b412d0cee2fe92f4493fde95964f87f7966860
Author: Sean Enck <sean@ttypty.com>
Date:   Fri, 16 Aug 2024 22:18:31 -0400

sequence rewrite allows sharing of singular external entity type for queries and transactions

Diffstat:
Minternal/app/move.go | 2+-
Minternal/app/remove.go | 11+----------
Minternal/backend/actions.go | 31+++++++++++++++++--------------
Minternal/backend/actions_test.go | 29++++++++++++++++-------------
Minternal/backend/core.go | 27++++++++-------------------
Minternal/backend/core_test.go | 29++++++++++-------------------
Minternal/backend/query.go | 24++++++++++++++----------
Minternal/backend/query_test.go | 2+-
8 files changed, 68 insertions(+), 87 deletions(-)

diff --git a/internal/app/move.go b/internal/app/move.go @@ -98,5 +98,5 @@ func (r moveRequest) do(dryRun bool) error { if dryRun { return nil } - return tx.Move(srcExists.Transaction(), r.dst) + return tx.Move(srcExists, r.dst) } diff --git a/internal/app/remove.go b/internal/app/remove.go @@ -4,8 +4,6 @@ package app import ( "errors" "fmt" - - "github.com/seanenck/lockbox/internal/backend" ) // Remove will remove an entry @@ -34,14 +32,7 @@ func Remove(cmd CommandOptions) error { fmt.Fprintln(w, "") } if cmd.Confirm(fmt.Sprintf("delete entr%s", postfixRemove)) { - removals := func() []backend.TransactionEntity { - var tx []backend.TransactionEntity - for _, e := range existings { - tx = append(tx, e.Transaction()) - } - return tx - }() - if err := t.RemoveAll(removals); err != nil { + if err := t.RemoveAll(existings); err != nil { return fmt.Errorf("unable to remove: %w", err) } } diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -182,11 +182,14 @@ func findAndDo(isAdd bool, entityName string, offset []string, opEntity *gokeepa } // Move will move a src object to a dst location -func (t *Transaction) Move(src TransactionEntity, dst string) error { - if strings.TrimSpace(src.path) == "" { +func (t *Transaction) Move(src *Entity, dst string) error { + if src == nil { + return errors.New("source entity is not set") + } + if strings.TrimSpace(src.Path) == "" { return errors.New("empty path not allowed") } - if strings.TrimSpace(src.value) == "" { + if strings.TrimSpace(src.Value) == "" { return errors.New("empty secret not allowed") } mod := config.EnvModTime.Get() @@ -202,22 +205,22 @@ func (t *Transaction) Move(src TransactionEntity, dst string) error { if err != nil { return err } - sOffset, sTitle, err := splitComponents(src.path) + sOffset, sTitle, err := splitComponents(src.Path) if err != nil { return err } action := MoveAction - if dst == src.path { + if dst == src.Path { action = InsertAction } - hook, err := NewHook(src.path, action) + hook, err := NewHook(src.Path, action) if err != nil { return err } if err := hook.Run(HookPre); err != nil { return err } - multi := len(strings.Split(strings.TrimSpace(src.value), "\n")) > 1 + multi := len(strings.Split(strings.TrimSpace(src.Value), "\n")) > 1 err = t.change(func(c Context) error { c.removeEntity(sOffset, sTitle) if action == MoveAction { @@ -229,7 +232,7 @@ func (t *Transaction) Move(src TransactionEntity, dst string) error { if multi { field = notesKey } - v := src.value + v := src.Value ok, err := isTOTP(dTitle) if err != nil { return err @@ -254,19 +257,19 @@ func (t *Transaction) Move(src TransactionEntity, dst string) error { // Insert is a move to the same location func (t *Transaction) Insert(path, val string) error { - return t.Move(TransactionEntity{path: path, value: val}, path) + return t.Move(&Entity{Path: path, Value: val}, path) } // Remove will remove a single entity -func (t *Transaction) Remove(entity *TransactionEntity) error { +func (t *Transaction) Remove(entity *Entity) error { if entity == nil { return errors.New("entity is empty/invalid") } - return t.RemoveAll([]TransactionEntity{*entity}) + return t.RemoveAll([]Entity{*entity}) } // RemoveAll handles removing elements -func (t *Transaction) RemoveAll(entities []TransactionEntity) error { +func (t *Transaction) RemoveAll(entities []Entity) error { if len(entities) == 0 { return errors.New("no entities given") } @@ -278,11 +281,11 @@ func (t *Transaction) RemoveAll(entities []TransactionEntity) error { removals := []removal{} hasHooks := false for _, entity := range entities { - offset, title, err := splitComponents(entity.path) + offset, title, err := splitComponents(entity.Path) if err != nil { return err } - hook, err := NewHook(entity.path, RemoveAction) + hook, err := NewHook(entity.Path, RemoveAction) if err != nil { return err } diff --git a/internal/backend/actions_test.go b/internal/backend/actions_test.go @@ -97,7 +97,10 @@ func TestMove(t *testing.T) { setup(t) fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass") fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass") - if err := fullSetup(t, true).Move(backend.QueryEntity{Path: backend.NewPath("test", "test2", "test3"), Value: "abc"}.Transaction(), backend.NewPath("test1", "test2", "test3")); err != nil { + if err := fullSetup(t, true).Move(nil, ""); err == nil || err.Error() != "source entity is not set" { + t.Errorf("no error: %v", err) + } + if err := fullSetup(t, true).Move(&backend.Entity{Path: backend.NewPath("test", "test2", "test3"), Value: "abc"}, backend.NewPath("test1", "test2", "test3")); err != nil { t.Errorf("no error: %v", err) } q, err := fullSetup(t, true).Get(backend.NewPath("test1", "test2", "test3"), backend.SecretValue) @@ -107,7 +110,7 @@ func TestMove(t *testing.T) { if q.Value != "abc" { t.Errorf("invalid retrieval") } - if err := fullSetup(t, true).Move(backend.QueryEntity{Path: backend.NewPath("test", "test2", "test1"), Value: "test"}.Transaction(), backend.NewPath("test1", "test2", "test3")); err != nil { + if err := fullSetup(t, true).Move(&backend.Entity{Path: backend.NewPath("test", "test2", "test1"), Value: "test"}, backend.NewPath("test1", "test2", "test3")); err != nil { t.Errorf("no error: %v", err) } q, err = fullSetup(t, true).Get(backend.NewPath("test1", "test2", "test3"), backend.SecretValue) @@ -176,10 +179,10 @@ func TestRemoves(t *testing.T) { if err := setup(t).Remove(nil); err.Error() != "entity is empty/invalid" { t.Errorf("wrong error: %v", err) } - if err := setup(t).Remove(&backend.TransactionEntity{}); err.Error() != "input paths must contain at LEAST 2 components" { + if err := setup(t).Remove(&backend.Entity{}); err.Error() != "input paths must contain at LEAST 2 components" { t.Errorf("wrong error: %v", err) } - tx := backend.QueryEntity{Path: backend.NewPath("test1", "test2", "test3")}.Transaction() + tx := backend.Entity{Path: backend.NewPath("test1", "test2", "test3")} if err := setup(t).Remove(&tx); err.Error() != "failed to remove entity" { t.Errorf("wrong error: %v", err) } @@ -187,14 +190,14 @@ func TestRemoves(t *testing.T) { for _, i := range []string{"test1", "test2"} { fullSetup(t, true).Insert(backend.NewPath(i, i, i), "pass") } - tx = backend.QueryEntity{Path: backend.NewPath("test1", "test1", "test1")}.Transaction() + tx = backend.Entity{Path: backend.NewPath("test1", "test1", "test1")} if err := fullSetup(t, true).Remove(&tx); err != nil { t.Errorf("wrong error: %v", err) } if err := check(t, backend.NewPath("test2", "test2", "test2")); err != nil { t.Errorf("invalid check: %v", err) } - tx = backend.QueryEntity{Path: backend.NewPath("test2", "test2", "test2")}.Transaction() + tx = backend.Entity{Path: backend.NewPath("test2", "test2", "test2")} if err := fullSetup(t, true).Remove(&tx); err != nil { t.Errorf("wrong error: %v", err) } @@ -202,35 +205,35 @@ func TestRemoves(t *testing.T) { for _, i := range []string{backend.NewPath("test", "test", "test1"), backend.NewPath("test", "test", "test2"), backend.NewPath("test", "test", "test3"), backend.NewPath("test", "test1", "test2"), backend.NewPath("test", "test1", "test5")} { fullSetup(t, true).Insert(i, "pass") } - tx = backend.QueryEntity{Path: "test/test/test3"}.Transaction() + tx = backend.Entity{Path: "test/test/test3"} if err := fullSetup(t, true).Remove(&tx); err != nil { t.Errorf("wrong error: %v", err) } if err := check(t, backend.NewPath("test", "test", "test2"), backend.NewPath("test", "test", "test1"), backend.NewPath("test", "test1", "test2"), backend.NewPath("test", "test1", "test5")); err != nil { t.Errorf("invalid check: %v", err) } - tx = backend.QueryEntity{Path: "test/test/test1"}.Transaction() + tx = backend.Entity{Path: "test/test/test1"} if err := fullSetup(t, true).Remove(&tx); err != nil { t.Errorf("wrong error: %v", err) } if err := check(t, backend.NewPath("test", "test", "test2"), backend.NewPath("test", "test1", "test2"), backend.NewPath("test", "test1", "test5")); err != nil { t.Errorf("invalid check: %v", err) } - tx = backend.QueryEntity{Path: "test/test1/test5"}.Transaction() + tx = backend.Entity{Path: "test/test1/test5"} if err := fullSetup(t, true).Remove(&tx); err != nil { t.Errorf("wrong error: %v", err) } if err := check(t, backend.NewPath("test", "test", "test2"), backend.NewPath("test", "test1", "test2")); err != nil { t.Errorf("invalid check: %v", err) } - tx = backend.QueryEntity{Path: "test/test1/test2"}.Transaction() + tx = backend.Entity{Path: "test/test1/test2"} if err := fullSetup(t, true).Remove(&tx); err != nil { t.Errorf("wrong error: %v", err) } if err := check(t, backend.NewPath("test", "test", "test2")); err != nil { t.Errorf("invalid check: %v", err) } - tx = backend.QueryEntity{Path: "test/test/test2"}.Transaction() + tx = backend.Entity{Path: "test/test/test2"} if err := fullSetup(t, true).Remove(&tx); err != nil { t.Errorf("wrong error: %v", err) } @@ -240,14 +243,14 @@ func TestRemoveAlls(t *testing.T) { if err := setup(t).RemoveAll(nil); err.Error() != "no entities given" { t.Errorf("wrong error: %v", err) } - if err := setup(t).RemoveAll([]backend.TransactionEntity{}); err.Error() != "no entities given" { + if err := setup(t).RemoveAll([]backend.Entity{}); err.Error() != "no entities given" { t.Errorf("wrong error: %v", err) } setup(t) for _, i := range []string{backend.NewPath("test", "test", "test1"), backend.NewPath("test", "test", "test2"), backend.NewPath("test", "test", "test3"), backend.NewPath("test", "test1", "test2"), backend.NewPath("test", "test1", "test5")} { fullSetup(t, true).Insert(i, "pass") } - if err := fullSetup(t, true).RemoveAll([]backend.TransactionEntity{backend.QueryEntity{Path: "test/test/test3"}.Transaction(), backend.QueryEntity{Path: "test/test/test1"}.Transaction()}); err != nil { + if err := fullSetup(t, true).RemoveAll([]backend.Entity{{Path: "test/test/test3"}, {Path: "test/test/test1"}}); err != nil { t.Errorf("wrong error: %v", err) } if err := check(t, backend.NewPath("test", "test", "test2"), backend.NewPath("test", "test1", "test2"), backend.NewPath("test", "test1", "test5")); err != nil { diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -27,7 +27,7 @@ const ( type ( // QuerySeq2 wraps the iteration for query entities - QuerySeq2 iter.Seq2[QueryEntity, error] + QuerySeq2 iter.Seq2[Entity, error] // Transaction handles the overall operation of the transaction Transaction struct { valid bool @@ -40,24 +40,13 @@ type ( Context struct { db *gokeepasslib.Database } - // QueryEntity is the result of a query - QueryEntity struct { - Path string - Value string - backing gokeepasslib.Entry - } - // TransactionEntity objects alter the database entities - TransactionEntity struct { - path string - value string + // Entity are database objects from results and transactional changes + Entity struct { + Path string + Value string } ) -// Transaction will convert a query entity to a transaction entity -func (e QueryEntity) Transaction() TransactionEntity { - return TransactionEntity{path: e.Path, value: e.Value} -} - // Load will load a kdbx file for transactions func Load(file string) (*Transaction, error) { return loadFile(file, true) @@ -186,7 +175,7 @@ func NewPath(segments ...string) string { } // Directory gets the offset location of the entry without the 'name' -func (e QueryEntity) Directory() string { +func (e Entity) Directory() string { return Directory(e.Path) } @@ -219,8 +208,8 @@ func IsDirectory(path string) bool { } // Collect will create a slice from an iterable set of query sequence results -func (s QuerySeq2) Collect() ([]QueryEntity, error) { - var entities []QueryEntity +func (s QuerySeq2) Collect() ([]Entity, error) { + var entities []Entity for entity, err := range s { if err != nil { return nil, err diff --git a/internal/backend/core_test.go b/internal/backend/core_test.go @@ -2,7 +2,6 @@ package backend_test import ( "errors" - "fmt" "testing" "github.com/seanenck/lockbox/internal/backend" @@ -32,14 +31,6 @@ func TestIsDirectory(t *testing.T) { } } -func TestQueryToTransaction(t *testing.T) { - q := backend.QueryEntity{Path: "abc", Value: "xyz"} - tx := q.Transaction() - if fmt.Sprintf("%v", tx) != "{abc xyz}" { - t.Errorf("invalid transaction: %v", tx) - } -} - func TestBase(t *testing.T) { b := backend.Base("") if b != "" { @@ -83,19 +74,19 @@ func TestDirectory(t *testing.T) { } func TestEntityDir(t *testing.T) { - q := backend.QueryEntity{Path: backend.NewPath("abc", "xyz")} + q := backend.Entity{Path: backend.NewPath("abc", "xyz")} if q.Directory() != "abc" { t.Error("invalid query directory") } - q = backend.QueryEntity{Path: backend.NewPath("abc", "xyz", "111")} + q = backend.Entity{Path: backend.NewPath("abc", "xyz", "111")} if q.Directory() != "abc/xyz" { t.Error("invalid query directory") } - q = backend.QueryEntity{Path: ""} + q = backend.Entity{Path: ""} if q.Directory() != "" { t.Error("invalid query directory") } - q = backend.QueryEntity{Path: backend.NewPath("abc")} + q = backend.Entity{Path: backend.NewPath("abc")} if q.Directory() != "" { t.Error("invalid query directory") } @@ -115,23 +106,23 @@ func TestNewSuffix(t *testing.T) { } func generateTestSeq(hasError, extra bool) backend.QuerySeq2 { - return func(yield func(backend.QueryEntity, error) bool) { - if !yield(backend.QueryEntity{}, nil) { + return func(yield func(backend.Entity, error) bool) { + if !yield(backend.Entity{}, nil) { return } - if !yield(backend.QueryEntity{}, nil) { + if !yield(backend.Entity{}, nil) { return } if hasError { - if !yield(backend.QueryEntity{}, errors.New("test collect error")) { + if !yield(backend.Entity{}, errors.New("test collect error")) { return } } - if !yield(backend.QueryEntity{}, nil) { + if !yield(backend.Entity{}, nil) { return } if extra { - if !yield(backend.QueryEntity{}, nil) { + if !yield(backend.Entity{}, nil) { return } } diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -55,7 +55,7 @@ const ( ) // MatchPath will try to match 1 or more elements (more elements when globbing) -func (t *Transaction) MatchPath(path string) ([]QueryEntity, error) { +func (t *Transaction) MatchPath(path string) ([]Entity, error) { if !strings.HasSuffix(path, isGlob) { e, err := t.Get(path, BlankValue) if err != nil { @@ -64,7 +64,7 @@ func (t *Transaction) MatchPath(path string) ([]QueryEntity, error) { if e == nil { return nil, nil } - return []QueryEntity{*e}, nil + return []Entity{*e}, nil } prefix := strings.TrimSuffix(path, isGlob) if strings.HasSuffix(prefix, pathSep) { @@ -74,7 +74,7 @@ func (t *Transaction) MatchPath(path string) ([]QueryEntity, error) { } // Get will request a singular entity -func (t *Transaction) Get(path string, mode ValueMode) (*QueryEntity, error) { +func (t *Transaction) Get(path string, mode ValueMode) (*Entity, error) { _, _, err := splitComponents(path) if err != nil { return nil, err @@ -108,7 +108,7 @@ func forEach(offset string, groups []gokeepasslib.Group, entries []gokeepasslib. } } -func (t *Transaction) queryCollect(args QueryOptions) ([]QueryEntity, error) { +func (t *Transaction) queryCollect(args QueryOptions) ([]Entity, error) { e, err := t.QueryCallback(args) if err != nil { return nil, err @@ -121,7 +121,11 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { if args.Mode == noneMode { return nil, errors.New("no query mode specified") } - var entities []QueryEntity + type entity struct { + path string + backing gokeepasslib.Entry + } + var entities []entity isSort := args.Mode != ExactMode decrypt := args.Values != BlankValue err := t.act(func(ctx Context) error { @@ -152,10 +156,10 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { } } } - obj := QueryEntity{backing: entry, Path: path} + obj := entity{backing: entry, path: path} if isSort && len(entities) > 0 { - i, _ := slices.BinarySearchFunc(entities, obj, func(i, j QueryEntity) int { - return strings.Compare(i.Path, j.Path) + i, _ := slices.BinarySearchFunc(entities, obj, func(i, j entity) int { + return strings.Compare(i.path, j.path) }) entities = slices.Insert(entities, i, obj) } else { @@ -185,9 +189,9 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { return nil, err } } - return func(yield func(QueryEntity, error) bool) { + return func(yield func(Entity, error) bool) { for _, item := range entities { - entity := QueryEntity{Path: item.Path} + entity := Entity{Path: item.path} var err error if args.Values != BlankValue { val := getValue(item.backing, notesKey) diff --git a/internal/backend/query_test.go b/internal/backend/query_test.go @@ -156,7 +156,7 @@ func TestValueModes(t *testing.T) { } } -func testCollect(t *testing.T, count int, seq backend.QuerySeq2) []backend.QueryEntity { +func testCollect(t *testing.T, count int, seq backend.QuerySeq2) []backend.Entity { collected, err := seq.Collect() if err != nil { t.Errorf("invalid collect error: %v", err)