package autofillcache import ( "encoding/json" "net/url" "os" "path/filepath" "sort" "strings" "time" "git.julianfamily.org/keepassgo/vault" ) type Entry struct { ID string `json:"id"` Title string `json:"title"` Username string `json:"username"` Password string `json:"password"` URL string `json:"url"` Host string `json:"host"` Path []string `json:"path,omitempty"` } type File struct { UpdatedAt string `json:"updatedAt"` Entries []Entry `json:"entries"` } func Match(cache File, webURL string) (Entry, bool) { target := normalizeURL(webURL) if target.host == "" { return Entry{}, false } exactHost := make([]Entry, 0) parentHost := make([]Entry, 0) for _, entry := range cache.Entries { if entry.Host == target.host { exactHost = append(exactHost, entry) continue } if entry.Host != "" && strings.HasSuffix(target.host, "."+entry.Host) { parentHost = append(parentHost, entry) } } if matched, ok := chooseEntry(target, exactHost); ok { return matched, true } return chooseEntry(target, parentHost) } func Build(model vault.Model, now time.Time) File { entries := make([]Entry, 0, len(model.Entries)) for _, item := range model.Entries { host := normalizeHost(item.URL) if host == "" { continue } if strings.TrimSpace(item.Username) == "" || strings.TrimSpace(item.Password) == "" { continue } entries = append(entries, Entry{ ID: item.ID, Title: item.Title, Username: item.Username, Password: item.Password, URL: item.URL, Host: host, Path: append([]string(nil), item.Path...), }) } return File{ UpdatedAt: now.UTC().Format(time.RFC3339), Entries: entries, } } func Write(path string, model vault.Model, now time.Time) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } data, err := json.MarshalIndent(Build(model, now), "", " ") if err != nil { return err } return os.WriteFile(path, data, 0o600) } func Clear(path string) error { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return err } return nil } func normalizeHost(raw string) string { return normalizeURL(raw).host } type normalizedTarget struct { host string path string url string } func normalizeURL(raw string) normalizedTarget { value := strings.TrimSpace(raw) if value == "" { return normalizedTarget{} } if !strings.Contains(value, "://") { value = "https://" + value } parsed, err := url.Parse(value) if err != nil { return normalizedTarget{} } host := strings.TrimSpace(parsed.Hostname()) path := cleanPath(parsed.EscapedPath()) return normalizedTarget{ host: strings.ToLower(host), path: path, url: strings.ToLower(host) + path, } } func cleanPath(path string) string { path = strings.TrimSpace(path) if path == "" || path == "/" { return "/" } path = strings.TrimRight(path, "/") if path == "" { return "/" } if !strings.HasPrefix(path, "/") { path = "/" + path } return path } func chooseEntry(target normalizedTarget, entries []Entry) (Entry, bool) { switch len(entries) { case 0: return Entry{}, false case 1: return entries[0], true } exact := make([]Entry, 0) prefix := make([]Entry, 0) for _, entry := range entries { entryTarget := normalizeURL(entry.URL) if entryTarget.host == "" { continue } if entryTarget.url == target.url { exact = append(exact, entry) continue } if entryTarget.path != "/" && strings.HasPrefix(target.path, entryTarget.path) { prefix = append(prefix, entry) } } if len(exact) == 1 { return exact[0], true } if len(exact) > 1 { return Entry{}, false } if len(prefix) == 0 { return Entry{}, false } sort.Slice(prefix, func(i, j int) bool { return len(normalizeURL(prefix[i].URL).path) > len(normalizeURL(prefix[j].URL).path) }) bestPath := normalizeURL(prefix[0].URL).path best := make([]Entry, 0, len(prefix)) for _, entry := range prefix { if normalizeURL(entry.URL).path == bestPath { best = append(best, entry) } } if len(best) == 1 { return best[0], true } return Entry{}, false }