<commit_before>//
//  Password.swift
//  pass
//
//  Created by <NAME> on 2/2/2017.
//  Copyright © 2017 <NAME>. All rights reserved.
//

import Foundation
import SwiftyUserDefaults
import OneTimePassword
import Base32
import Yams

struct AdditionField {
    var title: String
    var content: String
}

enum PasswordChange: Int {
    case path = 0x01
    case content = 0x02
    case none = 0x00
}

public class Password {
    public static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]
    private static let OTPAUTH = "otpauth"
    private static let OTPAUTH_URL_START = "\(OTPAUTH)://"

    public var name = ""
    public var url: URL?
    public var namePath: String {
        get {
            if url == nil {
                return ""
            }
            return url!.deletingPathExtension().path
        }
    }
    public var password = ""
    public var changed: Int = 0
    public var plainText = ""
    
    private var additions = [(String, String)]()
    private var firstLineIsOTPField = false
    private var otpToken: Token?
    
    public enum OtpType {
        case totp, hotp, none
    }
    
    public var otpType: OtpType {
        get {
            guard let token = self.otpToken else {
                return OtpType.none
            }
            switch token.generator.factor {
            case .counter:
                return OtpType.hotp
            case .timer:
                return OtpType.totp
            }
        }
    }
    
    public init(name: String, url: URL?, plainText: String) {
        self.initEverything(name: name, url: url, plainText: plainText)
    }
    
    public func updatePassword(name: String, url: URL?, plainText: String) {
        if self.plainText != plainText || self.url != url {
            if self.plainText != plainText {
                changed = changed|PasswordChange.content.rawValue
            }
            if self.url != url {
                changed = changed|PasswordChange.path.rawValue
            }
            self.initEverything(name: name, url: url, plainText: plainText)
        }
    }
    
    private func initEverything(name: String, url: URL?, plainText: String) {
        self.name = name
        self.url = url
        self.plainText = plainText
        additions.removeAll()
        
        // split the plain text
        let plainTextSplit = self.plainText.split(omittingEmptySubsequences: true) {
            $0 == "\n" || $0 == "\r\n"
            }.map(String.init)
    
        // get password
        password  = plainTextSplit.first ?? ""
        
        // get remaining lines
        let additionalLines = plainTextSplit[1...]
        
        // separate normal lines (no otp tokens)
        let normalAdditionalLines = additionalLines.filter {
            !$0.hasPrefix(Password.OTPAUTH_URL_START)
            }.joined(separator: "\n")
        
        // try to interpret the text format as YAML first
        do {
            try getAdditionalFields(fromYaml: normalAdditionalLines)
        }
        catch {
            getAdditionalFields(fromPlainText: normalAdditionalLines)
        }

        // get and append otp tokens
        let otpAdditionalLines = additionalLines.filter { $0.hasPrefix(Password.OTPAUTH_URL_START) }
        otpAdditionalLines.forEach { self.additions.append((Password.OTPAUTH, $0)) }
        
        // check whether the first line looks like an otp entry
        let (key, value) = Password.getKeyValuePair(from: self.password)
        if Password.otpKeywords.contains(key ?? "") {
            firstLineIsOTPField = true
            self.additions.append((key!, value))
        } else {
            firstLineIsOTPField = false
        }
        
        // construct the otp token
        self.updateOtpToken()
    }
    
    // check whether the file has lines with duplicated field names
    private func checkDuplicatedFields(lines: String) -> Bool {
        var keys = Set<String>()
        var hasDuplicatedFields = false
        lines.enumerateLines { (line, stop) -> () in
            let (key, _) = Password.getKeyValuePair(from: line)
            if let key = key {
                if keys.contains(key) {
                    hasDuplicatedFields = true
                    stop = true
                }
                keys.insert(key)
            }
        }
        return hasDuplicatedFields
    }

    private func getAdditionalFields(fromYaml: String) throws {
        guard !fromYaml.isEmpty else { return }
        if checkDuplicatedFields(lines: fromYaml) {
            throw AppError.YamlLoadError
        }
        let yamlFile = try Yams.load(yaml: fromYaml) as! [String: Any]
        additions.append(contentsOf: yamlFile.map { ($0, String(describing: $1)) })
    }

    private func getAdditionalFields(fromPlainText: String) {
        var unknownIndex = 0
        fromPlainText.enumerateLines() { line, _ in
            if !line.isEmpty {
                var (key, value) = Password.getKeyValuePair(from: line)
                if key == nil {
                    unknownIndex += 1
                    key = "unknown \(unknownIndex)"
                }
                self.additions.append((key!, value))
            }<commit_msg>Avoid crash during yaml parsing<commit_after>//
//  Password.swift
//  pass
//
//  Created by <NAME> on 2/2/2017.
//  Copyright © 2017 <NAME>. All rights reserved.
//

import Foundation
import SwiftyUserDefaults
import OneTimePassword
import Base32
import Yams

struct AdditionField {
    var title: String
    var content: String
}

enum PasswordChange: Int {
    case path = 0x01
    case content = 0x02
    case none = 0x00
}

public class Password {
    public static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]
    private static let OTPAUTH = "otpauth"
    private static let OTPAUTH_URL_START = "\(OTPAUTH)://"

    public var name = ""
    public var url: URL?
    public var namePath: String {
        get {
            if url == nil {
                return ""
            }
            return url!.deletingPathExtension().path
        }
    }
    public var password = ""
    public var changed: Int = 0
    public var plainText = ""
    
    private var additions = [(String, String)]()
    private var firstLineIsOTPField = false
    private var otpToken: Token?
    
    public enum OtpType {
        case totp, hotp, none
    }
    
    public var otpType: OtpType {
        get {
            guard let token = self.otpToken else {
                return OtpType.none
            }
            switch token.generator.factor {
            case .counter:
                return OtpType.hotp
            case .timer:
                return OtpType.totp
            }
        }
    }
    
    public init(name: String, url: URL?, plainText: String) {
        self.initEverything(name: name, url: url, plainText: plainText)
    }
    
    public func updatePassword(name: String, url: URL?, plainText: String) {
        if self.plainText != plainText || self.url != url {
            if self.plainText != plainText {
                changed = changed|PasswordChange.content.rawValue
            }
            if self.url != url {
                changed = changed|PasswordChange.path.rawValue
            }
            self.initEverything(name: name, url: url, plainText: plainText)
        }
    }
    
    private func initEverything(name: String, url: URL?, plainText: String) {
        self.name = name
        self.url = url
        self.plainText = plainText
        additions.removeAll()
        
        // split the plain text
        let plainTextSplit = self.plainText.split(omittingEmptySubsequences: true) {
            $0 == "\n" || $0 == "\r\n"
            }.map(String.init)
    
        // get password
        password  = plainTextSplit.first ?? ""
        
        // get remaining lines
        let additionalLines = plainTextSplit[1...]
        
        // separate normal lines (no otp tokens)
        let normalAdditionalLines = additionalLines.filter {
            !$0.hasPrefix(Password.OTPAUTH_URL_START)
            }.joined(separator: "\n")
        
        // try to interpret the text format as YAML first
        do {
            try getAdditionalFields(fromYaml: normalAdditionalLines)
        }
        catch {
            getAdditionalFields(fromPlainText: normalAdditionalLines)
        }

        // get and append otp tokens
        let otpAdditionalLines = additionalLines.filter { $0.hasPrefix(Password.OTPAUTH_URL_START) }
        otpAdditionalLines.forEach { self.additions.append((Password.OTPAUTH, $0)) }
        
        // check whether the first line looks like an otp entry
        let (key, value) = Password.getKeyValuePair(from: self.password)
        if Password.otpKeywords.contains(key ?? "") {
            firstLineIsOTPField = true
            self.additions.append((key!, value))
        } else {
            firstLineIsOTPField = false
        }
        
        // construct the otp token
        self.updateOtpToken()
    }
    
    // check whether the file has lines with duplicated field names
    private func checkDuplicatedFields(lines: String) -> Bool {
        var keys = Set<String>()
        var hasDuplicatedFields = false
        lines.enumerateLines { (line, stop) -> () in
            let (key, _) = Password.getKeyValuePair(from: line)
            if let key = key {
                if keys.contains(key) {
                    hasDuplicatedFields = true
                    stop = true
                }
                keys.insert(key)
            }
        }
        return hasDuplicatedFields
    }

    private func getAdditionalFields(fromYaml: String) throws {
        guard !fromYaml.isEmpty else { return }
        if checkDuplicatedFields(lines: fromYaml) {
            throw AppError.YamlLoadError
        }
        guard let yamlFile = try Yams.load(yaml: fromYaml) as? [String: Any] else {
            throw AppError.YamlLoadError
        }
        additions.append(contentsOf: yamlFile.map { ($0, String(describing: $1)) })
    }

    private func getAdditionalFields(fromPlainText: String) {
        var unknownIndex = 0
        fromPlainText.enumerateLines() { line, _ in
            if !line.isEmpty {
                var (key, value) = Password.getKeyValuePair(from: line)
                if key == nil {
                    unknownIndex += 1
                    key = "unknown \(unknownIndex)"
                }
                self.additions.append((key!, value))
            }
{'fraction_non_alphanumeric': 0.09197891169271323, 'fraction_numerical': 0.006025230653360949, 'mean_word_length': 1.7794348508634223, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2987405', 'n_tokens_mistral': 3038, 'n_tokens_neox': 2882, 'n_words': 778}