diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-17 08:45:18 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-17 08:45:18 +0300 |
| commit | eb89e32c6675d4ed50f66580346e5ea8f3d7b5b2 (patch) | |
| tree | 916d5299642622d3344778222e88db16aeace1ae /internal/cli | |
| parent | 9e2bf4af8b7b3b4ca2980aa6285482e7db0cd151 (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.go | 131 | ||||
| -rw-r--r-- | internal/cli/kdbx_store_test.go | 14 | ||||
| -rw-r--r-- | internal/cli/migrate_kdbx.go | 7 |
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) } |
