Add explicit vault view factories
This commit is contained in:
@@ -5,19 +5,27 @@ import "git.julianfamily.org/keepassgo/internal/vault"
|
||||
// HiddenRoot returns the single synthetic top-level vault group that should be
|
||||
// treated as an internal storage root rather than as a user-visible group.
|
||||
func HiddenRoot(model vault.Model) string {
|
||||
if len(model.EntriesInPath(nil)) != 0 {
|
||||
if !hasGroup(model.Groups, []string{KeepassRoot}) {
|
||||
return ""
|
||||
}
|
||||
groups := model.ChildGroups(nil)
|
||||
roots := make([]string, 0, len(groups))
|
||||
return KeepassRoot
|
||||
}
|
||||
|
||||
func hasGroup(groups [][]string, path []string) bool {
|
||||
for _, group := range groups {
|
||||
if group == "Recycle Bin" {
|
||||
if len(group) != len(path) {
|
||||
continue
|
||||
}
|
||||
roots = append(roots, group)
|
||||
match := true
|
||||
for i := range group {
|
||||
if group[i] != path[i] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if len(roots) != 1 {
|
||||
return ""
|
||||
}
|
||||
return roots[0]
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package vaultview
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"git.julianfamily.org/keepassgo/internal/vault"
|
||||
)
|
||||
|
||||
const KeepassRoot = "keepass"
|
||||
|
||||
// View projects the physical vault model into a logical tree for a specific
|
||||
// product surface.
|
||||
type View interface {
|
||||
ChildGroups(path []string) []string
|
||||
EntriesInPath(path []string) []vault.Entry
|
||||
EntriesUnderPath(path []string) []vault.Entry
|
||||
ToPhysicalPath(path []string) []string
|
||||
FromPhysicalPath(path []string) []string
|
||||
ToPhysicalEntry(entry vault.Entry) vault.Entry
|
||||
FromPhysicalEntry(entry vault.Entry) vault.Entry
|
||||
}
|
||||
|
||||
// Vault returns the physical datastore view.
|
||||
func Vault(model vault.Model) View {
|
||||
return physicalView{model: model}
|
||||
}
|
||||
|
||||
// VaultRoot returns the logical main-vault view rooted at the physical
|
||||
// keepass storage group.
|
||||
func VaultRoot(model vault.Model) View {
|
||||
return rootView{model: model}
|
||||
}
|
||||
|
||||
// VaultRecycleBin returns the logical recycle-bin view.
|
||||
func VaultRecycleBin(model vault.Model) View {
|
||||
return recycleBinView{model: model}
|
||||
}
|
||||
|
||||
type physicalView struct {
|
||||
model vault.Model
|
||||
}
|
||||
|
||||
func (v physicalView) ChildGroups(path []string) []string {
|
||||
return v.model.ChildGroups(path)
|
||||
}
|
||||
|
||||
func (v physicalView) EntriesInPath(path []string) []vault.Entry {
|
||||
return cloneEntries(v.model.EntriesInPath(path))
|
||||
}
|
||||
|
||||
func (v physicalView) EntriesUnderPath(path []string) []vault.Entry {
|
||||
return cloneEntries(v.model.EntriesUnderPath(path))
|
||||
}
|
||||
|
||||
func (v physicalView) ToPhysicalPath(path []string) []string {
|
||||
return clonePath(path)
|
||||
}
|
||||
|
||||
func (v physicalView) FromPhysicalPath(path []string) []string {
|
||||
return clonePath(path)
|
||||
}
|
||||
|
||||
func (v physicalView) ToPhysicalEntry(entry vault.Entry) vault.Entry {
|
||||
return cloneEntry(entry)
|
||||
}
|
||||
|
||||
func (v physicalView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
|
||||
return cloneEntry(entry)
|
||||
}
|
||||
|
||||
type rootView struct {
|
||||
model vault.Model
|
||||
}
|
||||
|
||||
func (v rootView) ChildGroups(path []string) []string {
|
||||
return v.model.ChildGroups(v.ToPhysicalPath(path))
|
||||
}
|
||||
|
||||
func (v rootView) EntriesInPath(path []string) []vault.Entry {
|
||||
return v.mapEntries(v.model.EntriesInPath(v.ToPhysicalPath(path)))
|
||||
}
|
||||
|
||||
func (v rootView) EntriesUnderPath(path []string) []vault.Entry {
|
||||
return v.mapEntries(v.model.EntriesUnderPath(v.ToPhysicalPath(path)))
|
||||
}
|
||||
|
||||
func (v rootView) ToPhysicalPath(path []string) []string {
|
||||
if len(path) == 0 {
|
||||
return []string{KeepassRoot}
|
||||
}
|
||||
return append([]string{KeepassRoot}, clonePath(path)...)
|
||||
}
|
||||
|
||||
func (v rootView) FromPhysicalPath(path []string) []string {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
if path[0] != KeepassRoot {
|
||||
return clonePath(path)
|
||||
}
|
||||
return clonePath(path[1:])
|
||||
}
|
||||
|
||||
func (v rootView) ToPhysicalEntry(entry vault.Entry) vault.Entry {
|
||||
entry = cloneEntry(entry)
|
||||
entry.Path = v.ToPhysicalPath(entry.Path)
|
||||
for i := range entry.History {
|
||||
entry.History[i].Path = v.ToPhysicalPath(entry.History[i].Path)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (v rootView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
|
||||
entry = cloneEntry(entry)
|
||||
entry.Path = v.FromPhysicalPath(entry.Path)
|
||||
for i := range entry.History {
|
||||
entry.History[i].Path = v.FromPhysicalPath(entry.History[i].Path)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (v rootView) mapEntries(entries []vault.Entry) []vault.Entry {
|
||||
out := make([]vault.Entry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
out = append(out, v.FromPhysicalEntry(entry))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type recycleBinView struct {
|
||||
model vault.Model
|
||||
}
|
||||
|
||||
func (v recycleBinView) ChildGroups(path []string) []string {
|
||||
return childGroups(v.model.RecycleBin, path)
|
||||
}
|
||||
|
||||
func (v recycleBinView) EntriesInPath(path []string) []vault.Entry {
|
||||
return entriesInPath(v.model.RecycleBin, path)
|
||||
}
|
||||
|
||||
func (v recycleBinView) EntriesUnderPath(path []string) []vault.Entry {
|
||||
var out []vault.Entry
|
||||
for _, entry := range v.model.RecycleBin {
|
||||
if len(path) > len(entry.Path) {
|
||||
continue
|
||||
}
|
||||
if !slices.Equal(entry.Path[:len(path)], path) {
|
||||
continue
|
||||
}
|
||||
out = append(out, cloneEntry(entry))
|
||||
}
|
||||
slices.SortFunc(out, func(a, b vault.Entry) int {
|
||||
switch {
|
||||
case a.Title < b.Title:
|
||||
return -1
|
||||
case a.Title > b.Title:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (v recycleBinView) ToPhysicalPath(path []string) []string {
|
||||
return clonePath(path)
|
||||
}
|
||||
|
||||
func (v recycleBinView) FromPhysicalPath(path []string) []string {
|
||||
return clonePath(path)
|
||||
}
|
||||
|
||||
func (v recycleBinView) ToPhysicalEntry(entry vault.Entry) vault.Entry {
|
||||
return cloneEntry(entry)
|
||||
}
|
||||
|
||||
func (v recycleBinView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
|
||||
return cloneEntry(entry)
|
||||
}
|
||||
|
||||
func childGroups(entries []vault.Entry, path []string) []string {
|
||||
seen := map[string]bool{}
|
||||
var groups []string
|
||||
for _, entry := range entries {
|
||||
if len(path) > len(entry.Path) {
|
||||
continue
|
||||
}
|
||||
if !slices.Equal(entry.Path[:len(path)], path) {
|
||||
continue
|
||||
}
|
||||
if len(entry.Path) == len(path) {
|
||||
continue
|
||||
}
|
||||
group := entry.Path[len(path)]
|
||||
if seen[group] {
|
||||
continue
|
||||
}
|
||||
seen[group] = true
|
||||
groups = append(groups, group)
|
||||
}
|
||||
slices.Sort(groups)
|
||||
return groups
|
||||
}
|
||||
|
||||
func entriesInPath(entries []vault.Entry, path []string) []vault.Entry {
|
||||
var out []vault.Entry
|
||||
for _, entry := range entries {
|
||||
if slices.Equal(entry.Path, path) {
|
||||
out = append(out, cloneEntry(entry))
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b vault.Entry) int {
|
||||
switch {
|
||||
case a.Title < b.Title:
|
||||
return -1
|
||||
case a.Title > b.Title:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneEntries(entries []vault.Entry) []vault.Entry {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]vault.Entry, len(entries))
|
||||
for i := range entries {
|
||||
out[i] = cloneEntry(entries[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneEntry(entry vault.Entry) vault.Entry {
|
||||
entry.Path = clonePath(entry.Path)
|
||||
entry.Tags = slices.Clone(entry.Tags)
|
||||
if entry.Fields != nil {
|
||||
fields := make(map[string]string, len(entry.Fields))
|
||||
for key, value := range entry.Fields {
|
||||
fields[key] = value
|
||||
}
|
||||
entry.Fields = fields
|
||||
}
|
||||
if entry.Attachments != nil {
|
||||
attachments := make(map[string][]byte, len(entry.Attachments))
|
||||
for key, value := range entry.Attachments {
|
||||
attachments[key] = slices.Clone(value)
|
||||
}
|
||||
entry.Attachments = attachments
|
||||
}
|
||||
if len(entry.History) != 0 {
|
||||
history := make([]vault.Entry, len(entry.History))
|
||||
for i := range entry.History {
|
||||
history[i] = cloneEntry(entry.History[i])
|
||||
}
|
||||
entry.History = history
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func clonePath(path []string) []string {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
return slices.Clone(path)
|
||||
}
|
||||
Reference in New Issue
Block a user