package vaultview import ( "slices" "git.julianfamily.org/keepassgo/internal/vault" ) const KeepassRoot = "keepass" const TemplatesRoot = "Templates" // 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 prefixedView{model: model, root: KeepassRoot, rooted: usesTopLevelRoot(model, KeepassRoot)} } // VaultTemplates returns the logical templates view rooted at the physical // Templates storage group. func VaultTemplates(model vault.Model) View { return templatesView{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 prefixedView struct { model vault.Model root string rooted bool } func (v prefixedView) ChildGroups(path []string) []string { return v.model.ChildGroups(v.ToPhysicalPath(path)) } func (v prefixedView) EntriesInPath(path []string) []vault.Entry { return v.mapEntries(v.model.EntriesInPath(v.ToPhysicalPath(path))) } func (v prefixedView) EntriesUnderPath(path []string) []vault.Entry { return v.mapEntries(v.model.EntriesUnderPath(v.ToPhysicalPath(path))) } func (v prefixedView) ToPhysicalPath(path []string) []string { if !v.rooted { return clonePath(path) } if len(path) == 0 { return []string{v.root} } return append([]string{v.root}, clonePath(path)...) } func (v prefixedView) FromPhysicalPath(path []string) []string { if !v.rooted { return clonePath(path) } if len(path) == 0 { return nil } if path[0] != v.root { return clonePath(path) } return clonePath(path[1:]) } func (v prefixedView) 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 prefixedView) 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 prefixedView) 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 } type templatesView struct { model vault.Model } func (v templatesView) ChildGroups(path []string) []string { return groupChildren(templateGroupPaths(v.model), v.EntriesUnderPath(nil), path) } func (v templatesView) EntriesInPath(path []string) []vault.Entry { return entriesInPath(v.EntriesUnderPath(nil), path) } func (v templatesView) EntriesUnderPath(path []string) []vault.Entry { var out []vault.Entry for _, entry := range v.model.Templates { if len(path) > len(entry.Path) { continue } physical := entry.Path if len(physical) > 0 && physical[0] == TemplatesRoot { physical = physical[1:] } if len(path) > len(physical) { continue } if !slices.Equal(physical[:len(path)], path) { continue } item := cloneEntry(entry) item.Path = clonePath(physical) for i := range item.History { item.History[i].Path = v.FromPhysicalPath(item.History[i].Path) } out = append(out, item) } 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 templatesView) ToPhysicalPath(path []string) []string { if len(path) == 0 { return []string{TemplatesRoot} } return append([]string{TemplatesRoot}, clonePath(path)...) } func (v templatesView) FromPhysicalPath(path []string) []string { if len(path) == 0 { return nil } if path[0] != TemplatesRoot { return clonePath(path) } return clonePath(path[1:]) } func (v templatesView) 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 templatesView) 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 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 { return groupChildren(nil, entries, path) } func groupChildren(groupPaths [][]string, 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) } for _, groupPath := range groupPaths { if len(path) > len(groupPath) { continue } if !slices.Equal(groupPath[:len(path)], path) { continue } if len(groupPath) == len(path) { continue } group := groupPath[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) } func templateGroupPaths(model vault.Model) [][]string { var out [][]string for _, group := range model.Groups { if len(group) == 0 || group[0] != TemplatesRoot { continue } out = append(out, clonePath(group[1:])) } return out } func usesTopLevelRoot(model vault.Model, root string) bool { if len(model.Entries) == 0 && len(model.Groups) == 0 && len(model.RecycleBin) == 0 { return root == KeepassRoot } return groupsUseRoot(model.Groups, root) || entriesUseRoot(model.Entries, root) || entriesUseRoot(model.Templates, root) || entriesUseRoot(model.RecycleBin, root) } func groupsUseRoot(groups [][]string, root string) bool { for _, group := range groups { if len(group) > 0 && group[0] == root { return true } } return false } func entriesUseRoot(entries []vault.Entry, root string) bool { for _, entry := range entries { if len(entry.Path) > 0 && entry.Path[0] == root { return true } } return false }