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) }