package com.peachpasswords.autofill /** * Site password constraints detected from a password field's HTML attributes * (exposed by [android.app.assist.AssistStructure.ViewNode.getHtmlInfo]). * * Android parity for the browser's `content/policy.ts` + `popup/generator-policy.ts`, * scoped to what the platform actually surfaces: AssistStructure carries a web * field's `minlength` / `maxlength` / `pattern` attributes, but NOT the surrounding * page prose the browser also scrapes ("8-20 characters, must contain a number"). * Used to size a generated password so the site will accept it. */ data class PasswordPolicy( val minLength: Int? = null, val maxLength: Int? = null, ) { /** * Length to generate at: the strongest password the site accepts. Mirrors * `generator-policy.ts` — prefer [maxLength] (capped at [hardMax]); otherwise * a strong [default]; then clamp into the site's bounds. */ fun generationLength(default: Int = DEFAULT_LENGTH, hardMax: Int = HARD_MAX): Int { val target = (maxLength ?: default).coerceAtMost(hardMax) val raised = minLength?.let { target.coerceAtLeast(it) } ?: target val capped = maxLength?.let { raised.coerceAtMost(it) } ?: raised return capped.coerceAtLeast(MIN_GENERATION_LENGTH) } /** True when the site's maximum is below the minimum strong-password length. */ val isWeaklyConstrained: Boolean get() = maxLength != null && maxLength < MIN_GENERATION_LENGTH /** Short human note about the active constraint, or empty when unconstrained. */ fun constraintNote(length: Int): String = when { isWeaklyConstrained -> "This site limits passwords to $maxLength characters (weak)" maxLength != null && length >= maxLength -> "This site allows max $maxLength characters" minLength != null && length <= minLength -> "This site requires at least $minLength characters" else -> "" } companion object { const val DEFAULT_LENGTH = 20 const val MIN_GENERATION_LENGTH = 12 const val HARD_MAX = 256 /** * Build a policy from a password field's HTML attributes (keys lower-cased). * Reads `minlength`/`maxlength` directly, then falls back to `pattern` * quantifiers — `.{N,M}` (range), `.{N,}` (min only), `.{N}` (exact) — the * same precedence policy.ts uses. Attribute values win over pattern. */ fun fromHtmlAttributes(attrs: Map): PasswordPolicy { var min = attrs["minlength"]?.toIntOrNull()?.takeIf { it > 0 } var max = attrs["maxlength"]?.toIntOrNull()?.takeIf { it > 0 } val pattern = attrs["pattern"] if (pattern != null && (min == null || max == null)) { val range = Regex("\\{(\\d+),(\\d+)\\}").find(pattern) val minOnly = Regex("\\{(\\d+),\\}").find(pattern) val exact = Regex("\\{(\\d+)\\}").find(pattern) when { range != null -> { if (min == null) min = range.groupValues[1].toIntOrNull() if (max == null) max = range.groupValues[2].toIntOrNull() } minOnly != null -> if (min == null) min = minOnly.groupValues[1].toIntOrNull() exact != null -> { val n = exact.groupValues[1].toIntOrNull() if (min == null) min = n if (max == null) max = n } } } return PasswordPolicy(minLength = min, maxLength = max) } } }