commit 8ac00388a1d93e9537a81d6652d1286ffef2bef1
parent 49c7044f7bd6718b84e980c2fe1a30a260fa4659
Author: Sean Enck <sean@ttypty.com>
Date: Tue, 10 Jun 2025 09:53:17 -0400
all filters should use filepath.Match
Diffstat:
11 files changed, 88 insertions(+), 75 deletions(-)
diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go
@@ -212,15 +212,15 @@ func test(profile string) error {
r.run("echo y |", "rm test2/key1")
r.logAppend("echo")
r.run("", "ls")
- r.run("", "ls multiline")
+ r.run("", "ls */multiline/*")
r.run("", "ls url")
r.run("", "json")
- r.run("", "json 'multiline'")
+ r.run("", "json '*/multiline'")
r.logAppend("echo")
r.run("echo 5ae472abqdekjqykoyxk7hvc2leklq5n |", "insert test6/multiline/otp")
r.run(`printf "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=5ae472abqdekjqykoyxk7hvc2leklq5n" |`, "insert test10/key1/otp")
r.run("", "totp ls")
- r.run("", "totp ls rooted")
+ r.run("", "totp ls '**/**/rooted/*")
const grepTOTP = "| sed 's/^[[:space:]]*//g' | grep '^[0-9][0-9][0-9][0-9][0-9][0-9]$'"
r.run("", fmt.Sprintf("totp show test6/multiline/otp %s", grepTOTP))
r.run("", fmt.Sprintf("totp show test10/key1/otp %s", grepTOTP))
@@ -233,8 +233,8 @@ func test(profile string) error {
r.run("", fmt.Sprintf("conv \"%s\"", r.store))
r.run("echo y |", "rm test10/*")
r.logAppend("echo")
- r.run("echo y |", "rm test7/deeper")
- r.run("echo y |", "rm test7/deeper/ro")
+ r.run("echo y |", "rm test7/deeper/**/*")
+ r.run("echo y |", "rm test7/deeper/ro**/*")
r.run("echo y |", "rm test1/key1/password")
r.run("echo y |", "rm test1/key1")
r.logAppend("echo")
@@ -242,7 +242,7 @@ func test(profile string) error {
r.logAppend("echo")
r.run("", "ls")
r.run("", "groups")
- r.run("", "groups test9")
+ r.run("", "groups 'test9/**/*'")
r.run("echo y |", "unset test8/unset/password")
r.logAppend("echo")
r.run("", "ls")
@@ -283,14 +283,14 @@ func test(profile string) error {
// test json modes
c["json.mode"] = c.quoteString("plaintext")
r.writeConfig(c)
- r.run("", "json test6")
+ r.run("", "json test6/*")
c["json.mode"] = c.quoteString("empty")
r.writeConfig(c)
- r.run("", "json test6")
+ r.run("", "json test6/*")
c["json.mode"] = c.quoteString("hash")
c["json.hash_length"] = "3"
r.writeConfig(c)
- r.run("", "json test6")
+ r.run("", "json test6/*")
// clipboard
copyFile := filepath.Join(r.testDir, "clip.copy")
diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log
@@ -58,7 +58,6 @@ test4/multiline/notes
test5/multiline/notes
test6/multiline/notes
test6/multiline/password
-test7/deeper/root/url
{
"test1/key1": {
"modtime": "XXXX-XX-XX",
@@ -123,7 +122,6 @@ test7/deeper/root/url
test10/key1/otp
test6/multiline/otp
test7/deeper/rooted/otp
-test7/deeper/rooted/otp
XXXXXX
XXXXXX
XXXXXX
@@ -189,8 +187,8 @@ period: 30
"password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633"
}
delete entry? (y/N)
-no entities matching: test7/deeper
-no entities matching: test7/deeper/ro
+no entities matching: test7/deeper/**/*
+no entities matching: test7/deeper/ro**/*
no entities matching: test1/key1/password
delete entry? (y/N)
selected entities:
diff --git a/internal/app/conv.go b/internal/app/conv.go
@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"io"
- "regexp"
"strings"
"git.sr.ht/~enckse/lockbox/internal/kdbx"
@@ -32,15 +31,7 @@ func Conv(cmd CommandOptions) error {
}
func serialize(w io.Writer, tx *kdbx.Transaction, isJSON bool, filter string) error {
- var re *regexp.Regexp
- hasFilter := filter != ""
- if hasFilter {
- var err error
- re, err = regexp.Compile(filter)
- if err != nil {
- return err
- }
- }
+ hasFilter, selector := createFilter(filter)
e, err := tx.QueryCallback(kdbx.QueryOptions{Mode: kdbx.ListMode, Values: kdbx.JSONValue})
if err != nil {
return err
@@ -54,7 +45,11 @@ func serialize(w io.Writer, tx *kdbx.Transaction, isJSON bool, filter string) er
return err
}
if hasFilter {
- if !re.MatchString(item.Path) {
+ ok, err := selector(filter, item.Path)
+ if err != nil {
+ return err
+ }
+ if !ok {
continue
}
}
diff --git a/internal/app/json_test.go b/internal/app/json_test.go
@@ -24,7 +24,7 @@ func TestJSON(t *testing.T) {
t.Error("no data")
}
m.buf = bytes.Buffer{}
- m.args = []string{"test2/test1"}
+ m.args = []string{"test/test2/*"}
if err := app.JSON(m); err != nil {
t.Errorf("invalid error: %v", err)
}
diff --git a/internal/app/list.go b/internal/app/list.go
@@ -4,7 +4,6 @@ package app
import (
"errors"
"fmt"
- "regexp"
"sort"
"strings"
@@ -28,26 +27,18 @@ func List(cmd CommandOptions, groups bool) error {
}
func doList(attr, filter string, cmd CommandOptions, groups bool) error {
- var re *regexp.Regexp
- hasFilter := filter != ""
- if hasFilter {
- var err error
- re, err = regexp.Compile(filter)
- if err != nil {
- return err
- }
- }
+ hasFilter, selector := createFilter(filter)
opts := kdbx.QueryOptions{}
opts.Mode = kdbx.ListMode
e, err := cmd.Transaction().QueryCallback(opts)
if err != nil {
return err
}
- allowed := func(p string) bool {
+ allowed := func(p string) (bool, error) {
if hasFilter {
- return re.MatchString(p)
+ return selector(filter, p)
}
- return true
+ return true, nil
}
w := cmd.Writer()
attrFilter := attr != ""
@@ -56,7 +47,11 @@ func doList(attr, filter string, cmd CommandOptions, groups bool) error {
return err
}
if groups {
- if allowed(f.Path) {
+ ok, err := allowed(f.Path)
+ if err != nil {
+ return err
+ }
+ if ok {
fmt.Fprintf(w, "%s\n", f.Path)
}
continue
@@ -72,7 +67,11 @@ func doList(attr, filter string, cmd CommandOptions, groups bool) error {
}
}
path := kdbx.NewPath(f.Path, k)
- if allowed(path) {
+ ok, err := allowed(path)
+ if err != nil {
+ return err
+ }
+ if ok {
results = append(results, path)
}
}
@@ -84,3 +83,10 @@ func doList(attr, filter string, cmd CommandOptions, groups bool) error {
}
return nil
}
+
+func createFilter(filter string) (bool, func(string, string) (bool, error)) {
+ if filter == "" {
+ return false, nil
+ }
+ return true, kdbx.Glob
+}
diff --git a/internal/app/list_test.go b/internal/app/list_test.go
@@ -57,7 +57,7 @@ func TestList(t *testing.T) {
func TestFind(t *testing.T) {
m := newMockCommand(t)
m.args = []string{"["}
- if err := app.List(m, false); err == nil || !strings.Contains(err.Error(), "missing closing") {
+ if err := app.List(m, false); err == nil || !strings.Contains(err.Error(), "syntax error in pattern") {
t.Errorf("invalid error: %v", err)
}
if m.buf.String() != "" {
@@ -72,7 +72,7 @@ func TestFind(t *testing.T) {
t.Error("something listed")
}
m.buf.Reset()
- m.args = []string{"test"}
+ m.args = []string{"test/test2/**/*"}
if err := app.List(m, false); err != nil {
t.Errorf("invalid error: %v", err)
}
@@ -80,7 +80,7 @@ func TestFind(t *testing.T) {
t.Error("nothing listed")
}
m.buf.Reset()
- m.args = []string{"test"}
+ m.args = []string{"test/**/*"}
if err := app.List(m, true); err != nil {
t.Errorf("invalid error: %v", err)
}
diff --git a/internal/app/move_test.go b/internal/app/move_test.go
@@ -68,7 +68,7 @@ func TestMove(t *testing.T) {
if err := app.Move(m); err.Error() != "test/test2/test3 must be a path, not an entry" {
t.Errorf("invalid error: %v", err)
}
- m.args = []string{"test/*", "test/test2/"}
+ m.args = []string{"test/**/*", "test/test2/"}
if err := app.Move(m); err.Error() != "multiple moves can only be done at a leaf level" {
t.Errorf("invalid error: %v", err)
}
diff --git a/internal/app/totp_test.go b/internal/app/totp_test.go
@@ -257,7 +257,7 @@ func TestParseWindows(t *testing.T) {
func TestTOTPListFilter(t *testing.T) {
setupTOTP(t)
- args, _ := app.NewTOTPArguments([]string{"ls", "test"})
+ args, _ := app.NewTOTPArguments([]string{"ls", "test/**/**/*"})
m, opts := newMock(t)
if err := args.Do(opts); err != nil {
t.Errorf("invalid error: %v", err)
diff --git a/internal/kdbx/core.go b/internal/kdbx/core.go
@@ -23,7 +23,7 @@ var (
const (
titleKey = "Title"
pathSep = "/"
- isGlob = pathSep + "*"
+ isGlob = "*"
modTimeKey = "ModTime"
// OTPField is the totp storage attribute
OTPField = "otp"
diff --git a/internal/kdbx/query.go b/internal/kdbx/query.go
@@ -5,6 +5,7 @@ import (
"crypto/sha512"
"errors"
"fmt"
+ "path/filepath"
"slices"
"strings"
@@ -43,27 +44,13 @@ const (
FindMode
// ExactMode means an entity must MATCH the string exactly
ExactMode
- // PrefixMode allows for entities starting with a specific value
- PrefixMode
+ // GlobMode indicates use a glob/match to find results
+ GlobMode
)
// MatchPath will try to match 1 or more elements (more elements when globbing)
func (t *Transaction) MatchPath(path string) ([]Entity, 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 []Entity{*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.queryCollect(QueryOptions{Mode: PrefixMode, Criteria: prefix + pathSep, Values: BlankValue})
+ return t.queryCollect(QueryOptions{Mode: GlobMode, Criteria: path, Values: BlankValue})
}
// Get will request a singular entity
@@ -86,7 +73,7 @@ func (t *Transaction) Get(path string, mode ValueMode) (*Entity, error) {
}
}
-func forEach(offset string, groups []gokeepasslib.Group, entries []gokeepasslib.Entry, cb func(string, gokeepasslib.Entry)) {
+func forEach(offset string, groups []gokeepasslib.Group, entries []gokeepasslib.Entry, cb func(string, gokeepasslib.Entry) error) error {
for _, g := range groups {
o := ""
if offset == "" {
@@ -94,11 +81,16 @@ func forEach(offset string, groups []gokeepasslib.Group, entries []gokeepasslib.
} else {
o = NewPath(offset, g.Name)
}
- forEach(o, g.Groups, g.Entries, cb)
+ if err := forEach(o, g.Groups, g.Entries, cb); err != nil {
+ return err
+ }
}
for _, e := range entries {
- cb(offset, e)
+ if err := cb(offset, e); err != nil {
+ return err
+ }
}
+ return nil
}
func (t *Transaction) queryCollect(args QueryOptions) ([]Entity, error) {
@@ -109,6 +101,11 @@ func (t *Transaction) queryCollect(args QueryOptions) ([]Entity, error) {
return e.Collect()
}
+// Glob is the baseline query for globbing for results
+func Glob(criteria, path string) (bool, error) {
+ return filepath.Match(criteria, path)
+}
+
// QueryCallback will retrieve a query based on setting
func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) {
if args.Mode == noneMode {
@@ -122,7 +119,7 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) {
isSort := args.Mode != ExactMode
decrypt := args.Values != BlankValue
err := t.act(func(ctx Context) error {
- forEach("", ctx.db.Content.Root.Groups[0].Groups, ctx.db.Content.Root.Groups[0].Entries, func(offset string, entry gokeepasslib.Entry) {
+ forEach("", ctx.db.Content.Root.Groups[0].Groups, ctx.db.Content.Root.Groups[0].Entries, func(offset string, entry gokeepasslib.Entry) error {
path := getPathName(entry)
if offset != "" {
path = NewPath(offset, path)
@@ -131,17 +128,21 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) {
switch args.Mode {
case FindMode:
if !strings.Contains(path, args.Criteria) {
- return
+ return nil
+ }
+ case GlobMode:
+ ok, err := Glob(args.Criteria, path)
+ if err != nil {
+ return err
}
- case PrefixMode:
- if !strings.HasPrefix(path, args.Criteria) {
- return
+ if !ok {
+ return nil
}
}
} else {
if args.Mode == ExactMode {
if path != args.Criteria {
- return
+ return nil
}
}
}
@@ -154,6 +155,7 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) {
} else {
entities = append(entities, obj)
}
+ return nil
})
if decrypt {
return ctx.db.UnlockProtectedEntries()
diff --git a/internal/kdbx/query_test.go b/internal/kdbx/query_test.go
@@ -89,9 +89,6 @@ func TestMatchPath(t *testing.T) {
if len(q) != 4 {
t.Error("invalid entity result")
}
- if _, err := fullSetup(t, true).MatchPath("test/test//*"); err.Error() != "invalid match criteria, too many path separators" {
- t.Errorf("wrong error: %v", err)
- }
q, err = fullSetup(t, true).MatchPath("test/test*")
if err != nil {
t.Errorf("no error: %v", err)
@@ -101,6 +98,21 @@ func TestMatchPath(t *testing.T) {
}
}
+func TestGlob(t *testing.T) {
+ _, err := kdbx.Glob("[", "x")
+ if err == nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ ok, err := kdbx.Glob("a", "b")
+ if ok || err != nil {
+ t.Errorf("invalid result/error: %v", err)
+ }
+ ok, err = kdbx.Glob("a/*", "a/b")
+ if !ok || err != nil {
+ t.Errorf("invalid result/error: %v", err)
+ }
+}
+
func TestGet(t *testing.T) {
setupInserts(t)
q, err := fullSetup(t, true).Get("test/test/abc", kdbx.BlankValue)