-
-
Save nerdsupremacist/bd0ffb7ab5e8a8e31259b13418dc27a6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public protocol PasswordCheck { | |
associatedtype Check: PasswordCheck | |
@CheckBuilder | |
var body: Check { get } | |
} | |
public enum PasswordResult { | |
case allowed | |
case denied(reasons: [String]) | |
} | |
extension PasswordCheck { | |
public func check(password: String) -> PasswordResult { | |
switch internalCheck().check(password: password) { | |
case .allow: | |
return .allowed | |
case .deny(let reasons): | |
return .denied(reasons: reasons) | |
case .skip: | |
return .denied(reasons: []) | |
} | |
} | |
} | |
public struct DenyIfTooFewFromCharacterSet: PasswordCheck { | |
let name: String | |
let min: Int | |
let characterSet: CharacterSet | |
public init(name: String, _ min: Int, _ characterSet: CharacterSet) { | |
self.name = name | |
self.min = min | |
self.characterSet = characterSet | |
} | |
public var body: some PasswordCheck { | |
DenyIf("Too few characters are \(name). Min \(min)") { password in | |
password.unicodeScalars.filter { characterSet.contains($0) }.count < min | |
} | |
} | |
} | |
public struct DenyIfLettersRepeated: PasswordCheck { | |
public init() { } | |
public var body: some PasswordCheck { | |
DenyIf { password in | |
let repeated = Dictionary(grouping: password) { $0 }.filter { $0.value.count > 1 }.keys.map(String.init) | |
if !repeated.isEmpty { | |
let joined = repeated.joined(separator: ", ") | |
return .deny(reason: "You're not allowed to repeat characters. You repeated \(joined)") | |
} else { | |
return .skip | |
} | |
} | |
} | |
} | |
public struct DenyIfTooLong: PasswordCheck { | |
let max: Int | |
public init(_ max: Int) { | |
self.max = max | |
} | |
public var body: some PasswordCheck { | |
DenyIf("Password is too long. Max (\(max))") { password in | |
password.count > max | |
} | |
} | |
} | |
public struct DenyIfTooShort: PasswordCheck { | |
let min: Int | |
public init(_ min: Int) { | |
self.min = min | |
} | |
public var body: some PasswordCheck { | |
DenyIf("Password is too short. Min (\(min))") { password in | |
password.count < min | |
} | |
} | |
} | |
public struct DenyIfNotInCharacterSet: PasswordCheck { | |
let characterSet: CharacterSet | |
public init(_ characterSet: CharacterSet) { | |
self.characterSet = characterSet | |
} | |
public var body: some PasswordCheck { | |
DenyIf { password in | |
let notInCharacterSet = password.unicodeScalars.filter { !characterSet.contains($0) }.map(String.init) | |
if notInCharacterSet.isEmpty { | |
return .skip | |
} else { | |
return .deny(reason: "Characters \(notInCharacterSet.joined(separator: ", ")) are not allowed") | |
} | |
} | |
} | |
} | |
private enum Decision { | |
case allow, skip | |
case deny(reasons: [String]) | |
} | |
private protocol InternalCheck { | |
func check(password: String) -> Decision | |
} | |
extension Never: PasswordCheck { | |
public var body: Never { | |
return fatalError() | |
} | |
} | |
public struct AnyCheck: PasswordCheck, InternalCheck { | |
fileprivate let check: InternalCheck | |
public init<C : PasswordCheck>(_ check: C) { | |
self.check = check.internalCheck() | |
} | |
fileprivate func check(password: String) -> Decision { | |
return check.check(password: password) | |
} | |
public var body: Never { | |
return fatalError() | |
} | |
} | |
public struct CompoundCheck: PasswordCheck, InternalCheck { | |
fileprivate let checks: [InternalCheck] | |
public var body: Never { | |
return fatalError() | |
} | |
fileprivate func check(password: String) -> Decision { | |
var isDenied = false | |
var deniedReasons = [String]() | |
for check in checks { | |
switch check.check(password: password) { | |
case .allow where !isDenied: | |
return .allow | |
case .allow: | |
return .deny(reasons: deniedReasons) | |
case .deny(let reasons): | |
isDenied = true | |
deniedReasons += reasons | |
continue | |
case .skip: | |
continue | |
} | |
} | |
if isDenied { | |
return .deny(reasons: deniedReasons) | |
} | |
return .skip | |
} | |
} | |
extension PasswordCheck { | |
fileprivate func internalCheck() -> InternalCheck { | |
if let any = self as? AnyCheck { | |
return any.check | |
} | |
if let internalCheck = self as? InternalCheck { | |
return internalCheck | |
} | |
return body.internalCheck() | |
} | |
} | |
@_functionBuilder | |
public struct CheckBuilder { | |
public struct Expression { | |
fileprivate let check: InternalCheck | |
} | |
static func buildExpression<Check : PasswordCheck>(_ check: Check) -> Expression { | |
return Expression(check: check.internalCheck()) | |
} | |
static func buildBlock(_ expressions: Expression...) -> CompoundCheck { | |
return CompoundCheck(checks: expressions.map(\.check)) | |
} | |
} | |
public struct DenyIf: PasswordCheck, InternalCheck { | |
public enum DenyResult { | |
case deny(reason: String?) | |
case skip | |
} | |
let decision: (String) -> DenyResult | |
public init(_ decision: @escaping (String) -> DenyResult) { | |
self.decision = decision | |
} | |
public init(_ message: String? = nil, _ condition: @escaping (String) -> Bool) { | |
self.init { password in | |
if (condition(password)) { | |
return .deny(reason: message) | |
} else { | |
return .skip | |
} | |
} | |
} | |
public var body: Never { | |
return fatalError() | |
} | |
fileprivate func check(password: String) -> Decision { | |
switch decision(password) { | |
case .deny(let reason): | |
return .deny(reasons: reason.map { [$0] } ?? []) | |
case .skip: | |
return .skip | |
} | |
} | |
} | |
public struct AllowIf: PasswordCheck, InternalCheck { | |
let condition: (String) -> Bool | |
public init(_ condition: @escaping (String) -> Bool) { | |
self.condition = condition | |
} | |
public var body: Never { | |
return fatalError() | |
} | |
fileprivate func check(password: String) -> Decision { | |
if (condition(password)) { | |
return .allow | |
} | |
return .skip | |
} | |
} | |
public struct Allow: PasswordCheck, InternalCheck { | |
public init() { } | |
public var body: Never { | |
return fatalError() | |
} | |
fileprivate func check(password: String) -> Decision { | |
return .allow | |
} | |
} | |
public struct Deny: PasswordCheck, InternalCheck { | |
let message: String? | |
public init(_ message: String?) { | |
self.message = message | |
} | |
public var body: Never { | |
return fatalError() | |
} | |
fileprivate func check(password: String) -> Decision { | |
return .deny(reasons: message.map { [$0] } ?? []) | |
} | |
} | |
extension CharacterSet { | |
static let bmwPasswords = CharacterSet.lowercaseLetters.union(.decimalDigits).union(.symbols) | |
} | |
private let invalidPrefixes: Set<String> = [ | |
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", | |
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "appl", "ibm" | |
] | |
struct BMWPasswordCheck: PasswordCheck { | |
public var body: some PasswordCheck { | |
DenyIfNotInCharacterSet(.bmwPasswords) | |
DenyIfTooShort(8) | |
DenyIfTooLong(16) | |
DenyIfTooFewFromCharacterSet(name: "Lowercase Letters", 4, .lowercaseLetters) | |
DenyIfTooFewFromCharacterSet(name: "Numbers", 1, .decimalDigits) | |
DenyIfLettersRepeated() | |
DenyIf("Not allowed to use invalid prefixes") { password in | |
invalidPrefixes.contains { password.hasPrefix($0) } | |
} | |
Allow() | |
} | |
} | |
BMWPasswordCheck().check(password: "7q73*LUNb*ZxxVULYQ@R") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment