import Foundation public enum CredentialMatcher { public static func matches(entry: Entry, serviceIdentifier: String) -> Bool { guard !entry.url.isEmpty else { return false } guard let entryHost = host(from: entry.url), let serviceHost = host(from: serviceIdentifier) else { return entry.url.lowercased().contains(serviceIdentifier.lowercased()) } return hostsMatch(entryHost, serviceHost) } public static func filter( entries: [Entry], for serviceIdentifiers: [String] ) -> (suggested: [Entry], all: [Entry]) { guard !serviceIdentifiers.isEmpty else { return ([], entries) } let suggested = entries.filter { entry in serviceIdentifiers.contains { matches(entry: entry, serviceIdentifier: $0) } } let suggestedIDs = Set(suggested.map(\.id)) let rest = entries.filter { !suggestedIDs.contains($0.id) } return (suggested: suggested, all: rest) } private static func host(from urlString: String) -> String? { // KDBX entries and AutoFill service identifiers commonly omit the scheme // (e.g. "allocine.fr"), but URL(string:) only populates `.host` when an // authority component ("//") is present -- without it, the whole string // is parsed as a relative path and `.host` is nil. if let host = URL(string: urlString)?.host, !host.isEmpty { return host } return URL(string: "https://" + urlString)?.host } private static func hostsMatch(_ a: String, _ b: String) -> Bool { let na = stripped(a) let nb = stripped(b) return na == nb || na.hasSuffix("." + nb) || nb.hasSuffix("." + na) } private static func stripped(_ host: String) -> String { host.hasPrefix("www.") ? String(host.dropFirst(4)).lowercased() : host.lowercased() } }