428 lines
10 KiB
Go
428 lines
10 KiB
Go
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
|
|
}
|