Disambiguate same-host Android fill matches

This commit is contained in:
Joe Julian
2026-04-01 05:21:15 -07:00
parent 8be0aad14d
commit fbd364f01d
5 changed files with 336 additions and 15 deletions
@@ -35,20 +35,26 @@ final class AutofillCacheStore {
if (entries.isEmpty()) {
return null;
}
String normalizedDomain = normalizeHost(webDomain);
if (normalizedDomain.isEmpty()) {
return entries.get(0);
NormalizedTarget target = normalizeURL(webDomain);
if (target.host.isEmpty()) {
return null;
}
Entry fallback = null;
List<Entry> exactHost = new ArrayList<>();
List<Entry> parentHost = new ArrayList<>();
for (Entry entry : entries) {
if (entry.host.equals(normalizedDomain)) {
return entry;
if (entry.host.equals(target.host)) {
exactHost.add(entry);
continue;
}
if (fallback == null && normalizedDomain.endsWith("." + entry.host)) {
fallback = entry;
if (!entry.host.isEmpty() && target.host.endsWith("." + entry.host)) {
parentHost.add(entry);
}
}
return fallback;
Entry matched = chooseEntry(target, exactHost);
if (matched != null) {
return matched;
}
return chooseEntry(target, parentHost);
}
private static File findCacheFile(Context context) {
@@ -101,6 +107,7 @@ final class AutofillCacheStore {
String username = "";
String password = "";
String host = "";
String url = "";
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
@@ -114,6 +121,9 @@ final class AutofillCacheStore {
case "password":
password = nextString(reader);
break;
case "url":
url = nextString(reader);
break;
case "host":
host = normalizeHost(nextString(reader));
break;
@@ -123,7 +133,7 @@ final class AutofillCacheStore {
}
}
reader.endObject();
return new Entry(title, username, password, host);
return new Entry(title, username, password, host, url);
}
private static String nextString(JsonReader reader) throws IOException {
@@ -135,8 +145,12 @@ final class AutofillCacheStore {
}
private static String normalizeHost(String raw) {
return normalizeURL(raw).host;
}
private static NormalizedTarget normalizeURL(String raw) {
if (raw == null) {
return "";
return new NormalizedTarget("", "", "");
}
String value = raw.trim().toLowerCase(Locale.US);
if (value.startsWith("http://")) {
@@ -152,20 +166,113 @@ final class AutofillCacheStore {
if (colon >= 0) {
value = value.substring(0, colon);
}
String host = value;
String path = "/";
int schemeSep = raw.indexOf("://");
String original = raw.trim();
if (schemeSep < 0) {
original = "https://" + original;
}
try {
java.net.URI uri = java.net.URI.create(original);
if (uri.getHost() != null) {
host = uri.getHost().toLowerCase(Locale.US);
}
path = cleanPath(uri.getPath());
} catch (IllegalArgumentException ignored) {
path = "/";
}
return new NormalizedTarget(host, path, host + path);
}
private static String cleanPath(String raw) {
if (raw == null || raw.trim().isEmpty() || "/".equals(raw.trim())) {
return "/";
}
String value = raw.trim();
while (value.endsWith("/") && value.length() > 1) {
value = value.substring(0, value.length() - 1);
}
if (!value.startsWith("/")) {
value = "/" + value;
}
return value;
}
private static Entry chooseEntry(NormalizedTarget target, List<Entry> entries) {
if (entries.isEmpty()) {
return null;
}
if (entries.size() == 1) {
return entries.get(0);
}
List<Entry> exact = new ArrayList<>();
List<Entry> prefix = new ArrayList<>();
for (Entry entry : entries) {
NormalizedTarget entryTarget = normalizeURL(entry.url);
if (entryTarget.host.isEmpty()) {
continue;
}
if (entryTarget.url.equals(target.url)) {
exact.add(entry);
continue;
}
if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) {
prefix.add(entry);
}
}
if (exact.size() == 1) {
return exact.get(0);
}
if (exact.size() > 1 || prefix.isEmpty()) {
return null;
}
Entry best = null;
int bestLen = -1;
boolean ambiguous = false;
for (Entry entry : prefix) {
int pathLen = normalizeURL(entry.url).path.length();
if (pathLen > bestLen) {
best = entry;
bestLen = pathLen;
ambiguous = false;
} else if (pathLen == bestLen) {
ambiguous = true;
}
}
if (ambiguous) {
return null;
}
return best;
}
static final class Entry {
final String title;
final String username;
final String password;
final String host;
final String url;
Entry(String title, String username, String password, String host) {
Entry(String title, String username, String password, String host, String url) {
this.title = title;
this.username = username;
this.password = password;
this.host = host;
this.url = url;
}
}
private static final class NormalizedTarget {
final String host;
final String path;
final String url;
NormalizedTarget(String host, String path, String url) {
this.host = host;
this.path = path;
this.url = url;
}
}
}