PasswordPolicy.kt

3.3 KB · raw

1package com.peachpasswords.autofill23/**4 * Site password constraints detected from a password field's HTML attributes5 * (exposed by [android.app.assist.AssistStructure.ViewNode.getHtmlInfo]).6 *7 * Android parity for the browser's `content/policy.ts` + `popup/generator-policy.ts`,8 * scoped to what the platform actually surfaces: AssistStructure carries a web9 * field's `minlength` / `maxlength` / `pattern` attributes, but NOT the surrounding10 * page prose the browser also scrapes ("8-20 characters, must contain a number").11 * Used to size a generated password so the site will accept it.12 */13data class PasswordPolicy(14    val minLength: Int? = null,15    val maxLength: Int? = null,16) {17    /**18     * Length to generate at: the strongest password the site accepts. Mirrors19     * `generator-policy.ts` — prefer [maxLength] (capped at [hardMax]); otherwise20     * a strong [default]; then clamp into the site's bounds.21     */22    fun generationLength(default: Int = DEFAULT_LENGTH, hardMax: Int = HARD_MAX): Int {23        val target = (maxLength ?: default).coerceAtMost(hardMax)24        val raised = minLength?.let { target.coerceAtLeast(it) } ?: target25        val capped = maxLength?.let { raised.coerceAtMost(it) } ?: raised26        return capped.coerceAtLeast(1)27    }2829    /** Short human note about the active constraint, or empty when unconstrained. */30    fun constraintNote(length: Int): String = when {31        maxLength != null && length >= maxLength -> "This site allows max $maxLength characters"32        minLength != null && length <= minLength -> "This site requires at least $minLength characters"33        else -> ""34    }3536    companion object {37        const val DEFAULT_LENGTH = 2038        const val HARD_MAX = 1283940        /**41         * Build a policy from a password field's HTML attributes (keys lower-cased).42         * Reads `minlength`/`maxlength` directly, then falls back to `pattern`43         * quantifiers — `.{N,M}` (range), `.{N,}` (min only), `.{N}` (exact) — the44         * same precedence policy.ts uses. Attribute values win over pattern.45         */46        fun fromHtmlAttributes(attrs: Map<String, String>): PasswordPolicy {47            var min = attrs["minlength"]?.toIntOrNull()?.takeIf { it > 0 }48            var max = attrs["maxlength"]?.toIntOrNull()?.takeIf { it > 0 }49            val pattern = attrs["pattern"]50            if (pattern != null && (min == null || max == null)) {51                val range = Regex("\\{(\\d+),(\\d+)\\}").find(pattern)52                val minOnly = Regex("\\{(\\d+),\\}").find(pattern)53                val exact = Regex("\\{(\\d+)\\}").find(pattern)54                when {55                    range != null -> {56                        if (min == null) min = range.groupValues[1].toIntOrNull()57                        if (max == null) max = range.groupValues[2].toIntOrNull()58                    }59                    minOnly != null -> if (min == null) min = minOnly.groupValues[1].toIntOrNull()60                    exact != null -> {61                        val n = exact.groupValues[1].toIntOrNull()62                        if (min == null) min = n63                        if (max == null) max = n64                    }65                }66            }67            return PasswordPolicy(minLength = min, maxLength = max)68        }69    }70}