Separate parser and helpers from Password class for better testability

This commit is contained in:
Danny Moesch 2018-11-11 18:09:52 +01:00 committed by Bob Sun
parent 2abbceb2e9
commit 7c12263458
17 changed files with 913 additions and 537 deletions

View file

@ -0,0 +1,47 @@
//
// AdditionField.swift
// passKit
//
// Created by Danny Moesch on 30.09.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
public struct AdditionField: Hashable {
public let title: String, content: String
var asString: String {
return title.isEmpty ? content : title + ": " + content
}
var asTuple: (String, String) {
return (title, content)
}
}
extension AdditionField {
static func | (left: String, right: AdditionField) -> String {
return left | right.asString
}
static func | (left: AdditionField, right: String) -> String {
return left.asString | right
}
static func | (left: AdditionField, right: AdditionField) -> String {
return left.asString | right
}
}
extension AdditionField: Equatable {
public static func == (first: AdditionField, second: AdditionField) -> Bool {
return first.asTuple == second.asTuple
}
}
infix operator =>: MultiplicationPrecedence
func => (key: String, value: String) -> AdditionField {
return AdditionField(title: key, content: value)
}

View file

@ -0,0 +1,47 @@
//
// Constants.swift
// passKit
//
// Created by Danny Moesch on 16.08.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
public struct Constants {
public static let OTP_KEYWORDS = [
"otp_secret",
"otp_type",
"otp_algorithm",
"otp_period",
"otp_digits",
"otp_counter",
"otpauth",
]
static let BLANK = " "
static let MULTILINE_WITH_LINE_BREAK_INDICATOR = "|"
static let MULTILINE_WITH_LINE_BREAK_SEPARATOR = "\n"
static let MULTILINE_WITHOUT_LINE_BREAK_INDICATOR = ">"
static let MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR = BLANK
static let OTPAUTH = "otpauth"
static let OTPAUTH_URL_START = "\(OTPAUTH)://"
static let PASSWORD_KEYWORD = "password"
static let USERNAME_KEYWORD = "username"
static let LOGIN_KEYWORD = "login"
static let URL_KEYWORD = "url"
static let UNKNOWN = "unknown"
public static func isOtpRelated(line: String) -> Bool {
let (key, _) = Parser.getKeyValuePair(from: line)
return OTP_KEYWORDS.contains(key ?? "")
}
static func unknown(_ number: UInt) -> String {
return "\(UNKNOWN) \(number)"
}
static func getSeparator(breakingLines: Bool) -> String {
return breakingLines ? MULTILINE_WITH_LINE_BREAK_SEPARATOR : MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR
}
}

View file

@ -0,0 +1,93 @@
//
// Parser.swift
// passKit
//
// Created by Danny Moesch on 16.08.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
class Parser {
let firstLine: String
let additionsSection: String
let purgedAdditionalLines: [String]
// The parsing process is expensive. This field makes sure it is only done once if actually needed.
private(set) lazy var additionFields = getAdditionFields()
init(plainText: String) {
let splittedPlainText = plainText
.split(omittingEmptySubsequences: false) { $0 == "\n" || $0 == "\r\n" }
.map(String.init)
firstLine = splittedPlainText.first!
additionsSection = splittedPlainText[1...].joined(separator: "\n")
purgedAdditionalLines = splittedPlainText[1...].filter { !$0.isEmpty }
}
private func getAdditionFields() -> [AdditionField] {
var additions: [AdditionField] = []
var unknownIndex: UInt = 0
var i = purgedAdditionalLines.startIndex
while i < purgedAdditionalLines.count {
let line = purgedAdditionalLines[i]
i += 1
var (key, value) = Parser.getKeyValuePair(from: line)
if key == nil {
unknownIndex += 1
key = Constants.unknown(unknownIndex)
} else if value == Constants.MULTILINE_WITH_LINE_BREAK_INDICATOR {
value = gatherMultilineValue(startingAt: &i, removingLineBreaks: false)
} else if value == Constants.MULTILINE_WITHOUT_LINE_BREAK_INDICATOR {
value = gatherMultilineValue(startingAt: &i, removingLineBreaks: true)
}
additions.append(key! => value)
}
return additions
}
private func gatherMultilineValue(startingAt i: inout Int, removingLineBreaks: Bool) -> String {
var result = ""
guard i < purgedAdditionalLines.count else {
return result
}
let numberInitialBlanks = purgedAdditionalLines[i].enumerated().first {
$1 != Character(Constants.BLANK)
}?.0 ?? purgedAdditionalLines[i].count
guard numberInitialBlanks != 0 else {
return result
}
let initialBlanks = String(repeating: Constants.BLANK, count: numberInitialBlanks)
while i < purgedAdditionalLines.count && purgedAdditionalLines[i].starts(with: initialBlanks) {
result.append(String(purgedAdditionalLines[i].dropFirst(numberInitialBlanks)))
result.append(Constants.getSeparator(breakingLines: !removingLineBreaks))
i += 1
}
return result.trimmingCharacters(in: .whitespacesAndNewlines)
}
/// Split line from password file in to a key-value pair separted by `: `.
///
/// - Parameter line: Line from a password file
/// - Returns: Pair of two `String`s of which the first one can be 'nil'
static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
let items = line.components(separatedBy: ": ").map { String($0).trimmingCharacters(in: .whitespaces) }
var key: String?
var value = ""
if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) {
// No ': ' found, or empty on both sides of ': '.
value = line
// "otpauth" special case
if value.hasPrefix(Constants.OTPAUTH_URL_START) {
key = Constants.OTPAUTH
}
} else {
if !items[0].isEmpty {
key = items[0]
}
value = items[1]
}
return (key, value)
}
}