summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-17 08:45:18 +0300
committerPaul Buetow <paul@buetow.org>2026-04-17 08:45:18 +0300
commiteb89e32c6675d4ed50f66580346e5ea8f3d7b5b2 (patch)
tree916d5299642622d3344778222e88db16aeace1ae /internal/cli
parent9e2bf4af8b7b3b4ca2980aa6285482e7db0cd151 (diff)
refactor: deduplicate cli/kdbx_store.go using exported keepass helpers (tasks k4+l4)
Remove ~90 lines of duplicated code from kdbx_store.go by delegating to EnsureGroup, UpsertEntryByTitle, SetEntryField, SplitDescriptionPath, SanitizeRelativePath exported from internal/keepass. kdbxStore.Save() now delegates to keepass.AtomicSave, eliminating the double-close risk. migrate_kdbx.go updated to use the exported helpers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/kdbx_store.go131
-rw-r--r--internal/cli/kdbx_store_test.go14
-rw-r--r--internal/cli/migrate_kdbx.go7
3 files changed, 35 insertions, 117 deletions
diff --git a/internal/cli/kdbx_store.go b/internal/cli/kdbx_store.go
index 86c4f9a..bdaeb31 100644
--- a/internal/cli/kdbx_store.go
+++ b/internal/cli/kdbx_store.go
@@ -3,10 +3,10 @@ package cli
import (
"fmt"
"os"
- "path/filepath"
- "strings"
gokeepasslib "github.com/tobischo/gokeepasslib/v3"
+
+ "codeberg.org/snonux/foostore/internal/keepass"
)
// KDBXStore is the minimal interface needed by migrate-kdbx.
@@ -56,127 +56,40 @@ func OpenKDBXStore(dbPath, password string) (KDBXStore, error) {
}, nil
}
+// UpsertTextEntry creates or updates a text entry in groupPath with the given
+// title, password, and notes. Delegates field manipulation to keepass.SetEntryField
+// and group navigation to keepass.EnsureGroup to avoid duplication.
func (s *kdbxStore) UpsertTextEntry(groupPath []string, title, password, notes string) (bool, error) {
- g := s.ensureGroup(groupPath)
- entry, overwrote := upsertEntryByTitle(g, title)
- setEntryField(entry, "Title", title)
- setEntryField(entry, "Password", password)
- setEntryField(entry, "Notes", notes)
+ g := keepass.EnsureGroup(&s.db.Content.Root.Groups[0], groupPath)
+ entry, overwrote := keepass.UpsertEntryByTitle(g, title)
+ keepass.SetEntryField(entry, "Title", title)
+ keepass.SetEntryField(entry, "Password", password)
+ keepass.SetEntryField(entry, "Notes", notes)
return overwrote, nil
}
+// UpsertBinaryEntry creates or updates a binary attachment entry in groupPath.
+// Delegates field manipulation to keepass.SetEntryField and group navigation
+// to keepass.EnsureGroup to avoid duplication.
func (s *kdbxStore) UpsertBinaryEntry(groupPath []string, title, filename string, content []byte) (bool, error) {
- g := s.ensureGroup(groupPath)
- entry, overwrote := upsertEntryByTitle(g, title)
- setEntryField(entry, "Title", title)
- setEntryField(entry, "Password", "")
+ g := keepass.EnsureGroup(&s.db.Content.Root.Groups[0], groupPath)
+ entry, overwrote := keepass.UpsertEntryByTitle(g, title)
+ keepass.SetEntryField(entry, "Title", title)
+ keepass.SetEntryField(entry, "Password", "")
b := s.db.AddBinary(content)
entry.Binaries = []gokeepasslib.BinaryReference{b.CreateReference(filename)}
// Keep notes concise for binary-only entries.
- setEntryField(entry, "Notes", fmt.Sprintf("Migrated binary attachment: %s", filename))
+ keepass.SetEntryField(entry, "Notes", fmt.Sprintf("Migrated binary attachment: %s", filename))
return overwrote, nil
}
-func (s *kdbxStore) ensureGroup(groupPath []string) *gokeepasslib.Group {
- g := &s.db.Content.Root.Groups[0]
- for _, segment := range groupPath {
- if segment == "" {
- continue
- }
- found := -1
- for i := range g.Groups {
- if g.Groups[i].Name == segment {
- found = i
- break
- }
- }
- if found == -1 {
- ng := gokeepasslib.NewGroup()
- ng.Name = segment
- g.Groups = append(g.Groups, ng)
- found = len(g.Groups) - 1
- }
- g = &g.Groups[found]
- }
- return g
-}
-
-func upsertEntryByTitle(g *gokeepasslib.Group, title string) (*gokeepasslib.Entry, bool) {
- for i := range g.Entries {
- if g.Entries[i].GetTitle() == title {
- return &g.Entries[i], true
- }
- }
- e := gokeepasslib.NewEntry()
- g.Entries = append(g.Entries, e)
- return &g.Entries[len(g.Entries)-1], false
-}
-
+// Save locks protected entries and atomically writes the database to disk.
+// Delegates the tmp→encode→rename sequence to keepass.AtomicSave to avoid
+// duplicating that logic here (keepass.Backend.save() uses the same helper).
func (s *kdbxStore) Save() error {
if err := s.db.LockProtectedEntries(); err != nil {
return fmt.Errorf("locking kdbx entries: %w", err)
}
-
- tmpPath := s.path + ".tmp"
- out, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
- if err != nil {
- return fmt.Errorf("creating temporary kdbx %q: %w", tmpPath, err)
- }
- defer out.Close()
-
- if err := gokeepasslib.NewEncoder(out).Encode(s.db); err != nil {
- return fmt.Errorf("encoding kdbx to %q: %w", tmpPath, err)
- }
- if err := out.Close(); err != nil {
- return fmt.Errorf("closing temporary kdbx %q: %w", tmpPath, err)
- }
- if err := os.Rename(tmpPath, s.path); err != nil {
- return fmt.Errorf("replacing kdbx %q: %w", s.path, err)
- }
- return nil
-}
-
-func setEntryField(entry *gokeepasslib.Entry, key, value string) {
- for i := range entry.Values {
- if entry.Values[i].Key == key {
- entry.Values[i].Value.Content = value
- return
- }
- }
-
- entry.Values = append(entry.Values, gokeepasslib.ValueData{
- Key: key,
- Value: gokeepasslib.V{
- Content: value,
- },
- })
-}
-
-func splitDescriptionPath(description string) ([]string, string, error) {
- safePath, err := sanitizeRelativePath(description)
- if err != nil {
- return nil, "", err
- }
-
- parts := strings.Split(safePath, "/")
- if len(parts) == 1 {
- return nil, parts[0], nil
- }
- return parts[:len(parts)-1], parts[len(parts)-1], nil
-}
-
-func sanitizeRelativePath(path string) (string, error) {
- normalised := strings.ReplaceAll(path, "\\", "/")
- normalised = strings.TrimSpace(normalised)
- if normalised == "" {
- return "", fmt.Errorf("empty entry description")
- }
-
- clean := filepath.Clean(normalised)
- clean = strings.TrimPrefix(clean, "/")
- if clean == "." || clean == "" || clean == ".." || strings.HasPrefix(clean, "../") {
- return "", fmt.Errorf("unsafe entry description path %q", path)
- }
- return clean, nil
+ return keepass.AtomicSave(s.db, s.path)
}
diff --git a/internal/cli/kdbx_store_test.go b/internal/cli/kdbx_store_test.go
index fc79ca6..bff1a0a 100644
--- a/internal/cli/kdbx_store_test.go
+++ b/internal/cli/kdbx_store_test.go
@@ -1,11 +1,15 @@
package cli
-import "testing"
+import (
+ "testing"
+
+ "codeberg.org/snonux/foostore/internal/keepass"
+)
func TestSplitDescriptionPath(t *testing.T) {
- group, title, err := splitDescriptionPath("foo/bar/baz")
+ group, title, err := keepass.SplitDescriptionPath("foo/bar/baz")
if err != nil {
- t.Fatalf("splitDescriptionPath: %v", err)
+ t.Fatalf("SplitDescriptionPath: %v", err)
}
if title != "baz" {
t.Fatalf("title = %q; want baz", title)
@@ -16,8 +20,8 @@ func TestSplitDescriptionPath(t *testing.T) {
}
func TestSanitizeRelativePathRejectsTraversal(t *testing.T) {
- if _, err := sanitizeRelativePath("../secret"); err == nil {
- t.Fatal("sanitizeRelativePath should reject traversal path")
+ if _, err := keepass.SanitizeRelativePath("../secret"); err == nil {
+ t.Fatal("SanitizeRelativePath should reject traversal path")
}
}
diff --git a/internal/cli/migrate_kdbx.go b/internal/cli/migrate_kdbx.go
index f4bb58c..bc98d96 100644
--- a/internal/cli/migrate_kdbx.go
+++ b/internal/cli/migrate_kdbx.go
@@ -10,6 +10,7 @@ import (
"strings"
"time"
+ "codeberg.org/snonux/foostore/internal/keepass"
"codeberg.org/snonux/foostore/internal/store"
)
@@ -106,7 +107,7 @@ func (c *CLI) cmdMigrateKDBX(ctx context.Context, argv []string) int {
}
func (c *CLI) migrateOneEntry(ctx context.Context, idx *store.Index, opts migrateKDBXOptions, kdbx KDBXStore, stats *migrateKDBXStats) error {
- safePath, err := sanitizeRelativePath(idx.Description)
+ safePath, err := keepass.SanitizeRelativePath(idx.Description)
if err != nil {
return fmt.Errorf("entry %q: %w", idx.Description, err)
}
@@ -117,7 +118,7 @@ func (c *CLI) migrateOneEntry(ctx context.Context, idx *store.Index, opts migrat
}
if idx.IsBinary() {
- groupPath, title, err := splitDescriptionPath(safePath)
+ groupPath, title, err := keepass.SplitDescriptionPath(safePath)
if err != nil {
return fmt.Errorf("mapping binary entry %q: %w", idx.Description, err)
}
@@ -137,7 +138,7 @@ func (c *CLI) migrateOneEntry(ctx context.Context, idx *store.Index, opts migrat
return nil
}
- groupPath, title, err := splitDescriptionPath(safePath)
+ groupPath, title, err := keepass.SplitDescriptionPath(safePath)
if err != nil {
return fmt.Errorf("mapping text entry %q: %w", idx.Description, err)
}