lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 1230f22142630dbebad63e5a0429873fd6c32853
parent 82d2b4542af6c2da9aa4ec0528ab574ad9d99642
Author: Sean Enck <sean@ttypty.com>
Date:   Mon, 10 Oct 2022 20:28:03 -0400

allow glob based removal

Diffstat:
Mcmd/main.go | 12++++++++----
Minternal/backend/actions.go | 22++++++++++++++++------
Minternal/backend/actions_test.go | 19+++++++++++++++++++
Minternal/backend/query.go | 23+++++++++++++++++++++++
Minternal/backend/types.go | 3+++
Mtests/expected.log | 10+++++++++-
Mtests/run.sh | 9++++++++-
7 files changed, 86 insertions(+), 12 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -158,15 +158,19 @@ func run() *programError { return newError("rm requires a single entry", errors.New("missing argument")) } deleting := args[2] - existing, err := t.Get(deleting, backend.BlankValue) + postfixRemove := "y" + existings, err := t.MatchPath(deleting) if err != nil { return newError("unable to get entity to delete", err) } - if confirm("delete entry") { - if err := t.Remove(existing); err != nil { + + if len(existings) > 1 { + postfixRemove = "ies" + } + if confirm(fmt.Sprintf("delete entr%s", postfixRemove)) { + if err := t.RemoveAll(existings); err != nil { return newError("unable to remove entry", err) } - } case "show", "clip": if len(args) != 3 { diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -217,18 +217,28 @@ func (t *Transaction) Insert(path, val string) error { return t.Move(QueryEntity{Path: path, Value: val}, path) } -// Remove handles remove an element +// Remove will remove a single entity func (t *Transaction) Remove(entity *QueryEntity) error { if entity == nil { return errors.New("entity is empty/invalid") } - offset, title, err := splitComponents(entity.Path) - if err != nil { - return err + return t.RemoveAll([]QueryEntity{*entity}) +} + +// RemoveAll handles removing elements +func (t *Transaction) RemoveAll(entities []QueryEntity) error { + if len(entities) == 0 { + return errors.New("no entities given") } return t.change(func(c Context) error { - if ok := c.removeEntity(offset, title); !ok { - return errors.New("failed to remove entity") + for _, entity := range entities { + offset, title, err := splitComponents(entity.Path) + if err != nil { + return err + } + if ok := c.removeEntity(offset, title); !ok { + return errors.New("failed to remove entity") + } } return nil }) diff --git a/internal/backend/actions_test.go b/internal/backend/actions_test.go @@ -184,6 +184,25 @@ func TestRemoves(t *testing.T) { } } +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.QueryEntity{}); 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.QueryEntity{{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 { + t.Errorf("invalid check: %v", err) + } +} + func check(t *testing.T, checks ...string) error { tr := fullSetup(t, true) for _, c := range checks { diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -11,6 +11,25 @@ import ( "github.com/tobischo/gokeepasslib/v3" ) +// MatchPath will try to match 1 or more elements (more elements when globbing) +func (t *Transaction) MatchPath(path string) ([]QueryEntity, error) { + if !strings.HasSuffix(path, isGlob) { + e, err := t.Get(path, BlankValue) + if err != nil { + return nil, err + } + if e == nil { + return nil, nil + } + return []QueryEntity{*e}, nil + } + prefix := strings.TrimSuffix(path, isGlob) + if strings.HasSuffix(prefix, pathSep) { + return nil, errors.New("invalid match criteria, too many path separators") + } + return t.QueryCallback(QueryOptions{Mode: PrefixMode, Criteria: prefix + pathSep, Values: BlankValue}) +} + // Get will request a singular entity func (t *Transaction) Get(path string, mode ValueMode) (*QueryEntity, error) { _, _, err := splitComponents(path) @@ -71,6 +90,10 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) { if !strings.HasSuffix(path, args.Criteria) { return } + case PrefixMode: + if !strings.HasPrefix(path, args.Criteria) { + return + } } } else { diff --git a/internal/backend/types.go b/internal/backend/types.go @@ -49,6 +49,8 @@ const ( ExactMode // SuffixMode will look for an entity ending in a specific value SuffixMode + // PrefixMode allows for entities starting with a specific value + PrefixMode ) const ( @@ -65,6 +67,7 @@ const ( titleKey = "Title" passKey = "Password" pathSep = "/" + isGlob = pathSep + "*" ) var ( diff --git a/tests/expected.log b/tests/expected.log @@ -33,9 +33,17 @@ test/k/totp: hash:b6c44d5d8a75071d8e8a39df231b0b98584d1d42982b5cf230e44f94d9c48e2983e78955a54b70c0acb0428d6db7205101e332f950ffb6b6d643aa37287c6aa5 delete entry? (y/N) delete entry? (y/N) -delete entry? (y/N) unable to remove entry (entity is empty/invalid) +delete entry? (y/N) unable to remove entry (no entities given) keys/k/one2 keyx/d/e delete entry? (y/N) keys/k/one2 + + + +keys/k/one2 +keys/k2/one +keys/k2/one2 +delete entries? (y/N) +keys/k/one2 diff --git a/tests/run.sh b/tests/run.sh @@ -43,6 +43,13 @@ _run() { yes 2>/dev/null | "$BIN/lb" rm keyx/d/e echo "$BIN/lb" ls -} + echo "test2" | "$BIN/lb" insert keys/k2/one2 + echo "test" | "$BIN/lb" insert keys/k2/one + echo + "$BIN/lb" ls + yes 2>/dev/null | "$BIN/lb" rm keys/k2/* + echo + "$BIN/lb" ls + } _run 2>&1 | sed "s#$LOCKBOX_STORE##g" > $TESTS/actual.log