Skip to content

Commit

Permalink
Add password rule detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
grgar committed Oct 8, 2023
1 parent dfe069c commit 0b8759c
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 21 deletions.
2 changes: 2 additions & 0 deletions ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ struct ContentView: View {
}
}
.listStyle(.sidebar)
} content: {
EmptyView()
} detail: {
EmptyView()
}
Expand Down
6 changes: 2 additions & 4 deletions Favicon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,25 @@ import SwiftUI

struct Favicon: View {
let domain: String

var body: some View {
AsyncImage(url: URL(string: "https://\(domain)/favicon.ico")) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
} else if phase.error != nil {
Image(systemName: "ellipsis.circle")
.resizable()
.scaledToFit()
.opacity(0)
.accessibilityHidden(true)
} else {
Image(systemName: "ellipsis.circle")
.resizable()
.scaledToFit()
.redacted(reason: .placeholder)
.accessibilityHidden(true)
}
}
.scaledToFit()
.accessibilityHidden(true)
}
}
Expand Down
8 changes: 6 additions & 2 deletions Passwords Inspector.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
173245232AC4CE99005C3AC2 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173245222AC4CE99005C3AC2 /* Rule.swift */; };
173245252AC4D29D005C3AC2 /* PasswordRuleChips.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173245242AC4D29D005C3AC2 /* PasswordRuleChips.swift */; };
17618B7C2AC200C600B31C28 /* PasswordRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17618B7B2AC200C600B31C28 /* PasswordRules.swift */; };
17882AC52AC84AAD0037E3A6 /* PasswordRuleDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17882AC42AC84AAD0037E3A6 /* PasswordRuleDetail.swift */; };
17BFF7002ABCDDC2004047AD /* SharedCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BFF6FF2ABCDDC2004047AD /* SharedCredentials.swift */; };
17BFF7022ABF217D004047AD /* Favicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BFF7012ABF217D004047AD /* Favicon.swift */; };
17BFF7042ABF7D50004047AD /* Appended2FA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BFF7032ABF7D50004047AD /* Appended2FA.swift */; };
Expand All @@ -29,6 +30,7 @@
173245222AC4CE99005C3AC2 /* Rule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rule.swift; sourceTree = "<group>"; };
173245242AC4D29D005C3AC2 /* PasswordRuleChips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordRuleChips.swift; sourceTree = "<group>"; };
17618B7B2AC200C600B31C28 /* PasswordRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordRules.swift; sourceTree = "<group>"; };
17882AC42AC84AAD0037E3A6 /* PasswordRuleDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordRuleDetail.swift; sourceTree = "<group>"; };
17BFF6FF2ABCDDC2004047AD /* SharedCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedCredentials.swift; sourceTree = "<group>"; };
17BFF7012ABF217D004047AD /* Favicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favicon.swift; sourceTree = "<group>"; };
17BFF7032ABF7D50004047AD /* Appended2FA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appended2FA.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -73,6 +75,7 @@
173245222AC4CE99005C3AC2 /* Rule.swift */,
17618B7B2AC200C600B31C28 /* PasswordRules.swift */,
173245242AC4D29D005C3AC2 /* PasswordRuleChips.swift */,
17882AC42AC84AAD0037E3A6 /* PasswordRuleDetail.swift */,
);
path = Rule;
sourceTree = "<group>";
Expand Down Expand Up @@ -161,6 +164,7 @@
1717C4AD2ABA473F00AD991A /* ChangePasswordURLs.swift in Sources */,
1717C49E2ABA44EC00AD991A /* PIApp.swift in Sources */,
17BFF7002ABCDDC2004047AD /* SharedCredentials.swift in Sources */,
17882AC52AC84AAD0037E3A6 /* PasswordRuleDetail.swift in Sources */,
173245232AC4CE99005C3AC2 /* Rule.swift in Sources */,
17BFF7042ABF7D50004047AD /* Appended2FA.swift in Sources */,
17BFF7022ABF217D004047AD /* Favicon.swift in Sources */,
Expand Down Expand Up @@ -312,7 +316,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.georgegarside.Passwords-Inspector";
PRODUCT_BUNDLE_IDENTIFIER = com.georgegarside.passwordsinspector;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
Expand Down Expand Up @@ -349,7 +353,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.georgegarside.Passwords-Inspector";
PRODUCT_BUNDLE_IDENTIFIER = com.georgegarside.passwordsinspector;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
Expand Down
4 changes: 2 additions & 2 deletions Rule/PasswordRuleChips.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ struct PasswordRuleChips: View {
if let minLength = rule.minLength {
Image(systemName: "\(minLength).square")
}
if rule.minLength != nil && rule.maxLength != nil {
if rule.minLength != nil, let max = rule.maxLength, max <= 50 {
Text("")
}
if let maxLength = rule.maxLength {
if let maxLength = rule.maxLength, maxLength <= 50 {
Image(systemName: "\(maxLength).square")
}
}
Expand Down
128 changes: 128 additions & 0 deletions Rule/PasswordRuleDetail.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import SwiftUI

struct PasswordRuleDetail: View {
let rule: Rule

var body: some View {
List {
Section {
if rule.minLength != nil || rule.maxLength != nil {
HStack(alignment: .firstTextBaseline) {
if let min = rule.minLength {
VStack {
Image(systemName: "\(min).square.fill")
.font(.largeTitle)
Text("min")
}
}
if rule.minLength != nil && rule.maxLength != nil {
Text("")
.font(.largeTitle)
.foregroundStyle(.secondary)
}
if let min = rule.maxLength {
VStack {
Image(systemName: "\(min).square.fill")
.font(.largeTitle)
Text("max")
}
}
}
.foregroundStyle(.foreground, .quaternary)
.symbolRenderingMode(.palette)
.frame(maxWidth: .infinity)
} else {
Text("Unspecified")
}
} header: {
Text("Length")
}

Section {
Grid {
GridRow {
Text("")
.accessibilityHidden(true)
.gridCellUnsizedAxes([.horizontal, .vertical])
.gridColumnAlignment(.trailing)
Color.clear
.gridCellUnsizedAxes(.vertical)
.gridColumnAlignment(.leading)
Text("Required")
Text("Allowed")
}
.font(.caption)

ForEach(rule.required.sorted()) { required in
GridRow {
if let symbol = required.symbol {
Image(systemName: symbol)
} else {
Text("")
.accessibilityHidden(true)
.gridCellUnsizedAxes([.horizontal, .vertical])
}
switch required {
case let .other(set):
Text(set.sorted().map(String.init).joined())
default:
Text(required.description)
}
Image(systemName: "checkmark.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.foreground, .quaternary)
Image(systemName: "checkmark.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.foreground, .quaternary)
}
.padding(.vertical, 2)
}

ForEach((rule.allowed.subtracting(rule.required)).sorted()) { required in
GridRow {
if let symbol = required.symbol {
Image(systemName: symbol)
} else {
Text("")
.accessibilityHidden(true)
.gridCellUnsizedAxes([.horizontal, .vertical])
}
switch required {
case let .other(set):
Text(set.sorted().map(String.init).joined())
default:
Text(required.description)
}
Text("")
.accessibilityHidden(true)
Image(systemName: "checkmark.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.foreground, .quaternary)
}
.padding(.vertical, 2)
}
}
.padding(.vertical, 8)
} header: {
Text("Characters")
}
}
.navigationTitle(rule.id)
}
}

#Preview("Demo") {
PasswordRuleDetail(rule: .init(
id: "example.com", originalRule: "one: two; three: four; five: six;",
minLength: 8, maxLength: 16,
required: .init(arrayLiteral: .lower, .upper, .digit),
allowed: .init(arrayLiteral: .lower, .upper, .digit, .special, .unicode, .other(.init(arrayLiteral: "a", "b")))
))
}

#Preview("admiral.com") {
PasswordRuleDetail(rule: .init(
domain: "admiral.com",
rule: "minlength: 8; required: digit; required: [- !\"#$&'()*+,.:;<=>?@[^_`{|}~]]; allowed: lower, upper;"
))
}
2 changes: 1 addition & 1 deletion Rule/PasswordRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct PasswordRules: View {
List {
ForEach(responses) { rule in
NavigationLink {
Text("Destination")
PasswordRuleDetail(rule: rule)
} label: {
Label {
LabeledContent {} label: {
Expand Down
39 changes: 27 additions & 12 deletions Rule/Rule.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import RegexBuilder

struct Rule: Identifiable {
let id: String
Expand All @@ -10,7 +11,7 @@ struct Rule: Identifiable {
var allowed = Set<PasswordCharacter>()

enum PasswordCharacter: LosslessStringConvertible, Hashable, CaseIterable, Comparable, Identifiable {
case lower, upper, digit, special, unicode, other(CharacterSet)
case lower, upper, digit, special, unicode, other(Set<Character>)

init?(_ description: String) {
switch description {
Expand All @@ -25,7 +26,9 @@ struct Rule: Identifiable {
case "unicode":
self = .unicode
default:
self = .other(CharacterSet(charactersIn: description))
self = .other(description.reduce(into: Set<Character>()) { partialResult, character in
partialResult.insert(character)
})
}
}

Expand All @@ -36,10 +39,10 @@ struct Rule: Identifiable {
case .digit: return "digit"
case .special: return "special"
case .unicode: return "unicode"
case let .other(set): return set.description
case let .other(set): return set.sorted().map(String.init).joined()
}
}

var symbol: String? {
switch self {
case .lower: return "textformat.abc"
Expand All @@ -49,9 +52,9 @@ struct Rule: Identifiable {
default: return nil
}
}
var id: String { description }

var id: String { self.description }

var isPredefined: Bool {
switch self {
case .lower, .upper, .digit, .special, .unicode:
Expand All @@ -60,17 +63,20 @@ struct Rule: Identifiable {
return false
}
}

static let allCases: [Rule.PasswordCharacter] = [.upper, .lower, .digit, .special, .unicode, .other(.init())]

static func < (lhs: Rule.PasswordCharacter, rhs: Rule.PasswordCharacter) -> Bool {
switch (lhs.isPredefined, rhs.isPredefined) {
case (false, false), (false, true):
case (false, false):
guard case let .other(set1) = lhs, case let .other(set2) = rhs else { return false }
return set1.sorted().map(String.init).joined() < set2.sorted().map(String.init).joined()
case (false, true):
return false
case (true, false):
return true
case (true, true):
return (allCases.firstIndex(of: lhs) ?? 0) < (allCases.firstIndex(of: rhs) ?? 0)
return (self.allCases.firstIndex(of: lhs) ?? 0) < (self.allCases.firstIndex(of: rhs) ?? 0)
}
}
}
Expand All @@ -79,8 +85,8 @@ struct Rule: Identifiable {
extension Rule {
init(domain: String, rule originalRule: String) {
self = Self(id: domain, originalRule: originalRule)
for split in originalRule.split(separator: try! Regex(";(?: |$)"), omittingEmptySubsequences: true) {
let split = String(split).split(separator: try! Regex(": ?"), maxSplits: 2)
for split in originalRule.split(separator: /;(?: |$)/, omittingEmptySubsequences: true) {
let split = String(split).split(separator: /: ?/, maxSplits: 1)
if split.count != 2 { continue }
switch split[0] {
case "minlength":
Expand All @@ -107,3 +113,12 @@ extension Rule {
self.allowed.subtract(self.required)
}
}

extension CharacterSet {
static let descriptionRegex = /\((.*)\)|Predefined (.*) Set/

var contents: String {
let matches = try? Self.descriptionRegex.firstMatch(in: description)
return (matches?.1 ?? matches?.2)?.replacingOccurrences(of: "U+", with: "\\u").applyingTransform(.init("Hex/Unicode-Any"), reverse: false)?.lowercased() ?? description
}
}

0 comments on commit 0b8759c

Please sign in to comment.