forked from apple/password-manager-resources
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpassword-rules-parse.js
More file actions
118 lines (105 loc) · 4.25 KB
/
Copy pathpassword-rules-parse.js
File metadata and controls
118 lines (105 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// Copyright (c) 2026 Apple Inc. Licensed under MIT License.
//
// Lints every "password-rules" string in quirks/password-rules.json:
//
// 1. Validates it parses cleanly by loading tools/PasswordRulesParser.js and
// calling parsePasswordRules() on it. Any parse error (e.g. ",required"
// instead of "; required" as in #907, or '-'/']' in the wrong position
// inside a character class) causes CI to fail with a clear message
// identifying the offending domain.
//
// 2. Flags entries whose "allowed" list redundantly names a class that is
// already covered by a "required" rule. Required rules are implicitly
// allowed, so writing e.g. `required: upper, lower; allowed: upper,
// lower, [!@#];` is redundant and should be shortened to
// `required: upper, lower; allowed: [!@#];`.
"use strict";
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const repoRoot = path.resolve(__dirname, "..", "..", "..");
const parserPath = path.join(repoRoot, "tools", "PasswordRulesParser.js");
const rulesPath = path.join(repoRoot, "quirks", "password-rules.json");
const parserSource = fs.readFileSync(parserPath, "utf8");
const rules = JSON.parse(fs.readFileSync(rulesPath, "utf8"));
const sandbox = {
capturedErrors: [],
console: {
assert() {},
warn() {},
error(message) { sandbox.capturedErrors.push(String(message)); },
},
};
vm.createContext(sandbox);
vm.runInContext(parserSource, sandbox, { filename: parserPath });
// These are semantic supersets used to say "allow everything"; they subsume required
// classes by design and are not redundant.
const ALWAYS_ALLOWED_SUPERSETS = new Set(["ascii-printable", "unicode"]);
function namedClassNames(value) {
if (!Array.isArray(value)) {
return [];
}
const names = [];
for (const entry of value) {
// NamedCharacterClass instances have a `name` getter and no `characters`.
if (entry && typeof entry.name === "string" && !("characters" in entry)) {
names.push(entry.name);
}
}
return names;
}
function findRedundantAllowedClasses(parsedRules) {
const requiredNames = new Set();
let allowedRule = null;
for (const rule of parsedRules) {
if (rule.name === "required") {
for (const name of namedClassNames(rule.value)) {
requiredNames.add(name);
}
} else if (rule.name === "allowed") {
allowedRule = rule;
}
}
if (!allowedRule || requiredNames.size === 0) {
return [];
}
return namedClassNames(allowedRule.value)
.filter((name) => !ALWAYS_ALLOWED_SUPERSETS.has(name))
.filter((name) => requiredNames.has(name));
}
let failedDomains = 0;
for (const domain of Object.keys(rules)) {
const ruleString = rules[domain]["password-rules"];
sandbox.capturedErrors.length = 0;
sandbox.__ruleString = ruleString;
let parsed = null;
try {
// Pass formatRulesForMinifiedVersion=true so required classes are not
// silently copied into the allowed rule.
parsed = vm.runInContext("parsePasswordRules(__ruleString, true)", sandbox);
} catch (e) {
sandbox.capturedErrors.push(`threw ${e.message}`);
}
// Log parse errors and warnings before required/allowed redundancy issues.
const messages = sandbox.capturedErrors.slice();
if (parsed) {
const redundant = findRedundantAllowedClasses(parsed);
if (redundant.length > 0) {
const list = redundant.map((n) => `\`${n}\``).join(", ");
messages.push(`Redundancy in \`allowed:\` (already covered by \`required:\`): ${redundant.join(", ")}`);
messages.push(`Remove ${list} from the \`allowed:\` list — required classes are implicitly allowed.`);
}
}
if (messages.length > 0) {
failedDomains++;
console.error(`${domain}: ${ruleString}`);
for (const message of messages) {
console.error(` ${message}`);
}
}
}
if (failedDomains > 0) {
console.error(`\n${failedDomains} domain(s) in password-rules.json failed linting.`);
process.exit(1);
}
console.log(`OK: ${Object.keys(rules).length} password-rules entries parsed cleanly with no redundant required/allowed classes.`);