<commit_before>//
//  Token+URL.swift
//  OneTimePassword
//
//  Copyright (c) 2014-2016 <NAME> and the OneTimePassword authors
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//

import Foundation
import Base32

extension Token {
    // MARK: Serialization

    /// Serializes the token to a URL.
    public func toURL() throws -> NSURL {
        return try urlForToken(
            name: name,
            issuer: issuer,
            factor: generator.factor,
            algorithm: generator.algorithm,
            digits: generator.digits
        )
    }

    /// Attempts to initialize a token represented by the give URL.
    public init?(url: NSURL, secret: NSData? = nil) {
        if let token = tokenFromURL(url, secret: secret) {
            self = token
        } else {
            return nil
        }
    }
}

internal enum SerializationError: ErrorType {
    case URLGenerationFailure
}

private let defaultAlgorithm: Generator.Algorithm = .SHA1
private let defaultDigits: Int = 6
private let defaultCounter: UInt64 = 0
private let defaultPeriod: NSTimeInterval = 30

private let kOTPAuthScheme = "otpauth"
private let kQueryAlgorithmKey = "algorithm"
private let kQuerySecretKey = "secret"
private let kQueryCounterKey = "counter"
private let kQueryDigitsKey = "digits"
private let kQueryPeriodKey = "period"
private let kQueryIssuerKey = "issuer"

private let kFactorCounterKey = "hotp"
private let kFactorTimerKey = "totp"

private let kAlgorithmSHA1   = "SHA1"
private let kAlgorithmSHA256 = "SHA256"
private let kAlgorithmSHA512 = "SHA512"

private func stringForAlgorithm(algorithm: Generator.Algorithm) -> String {
    switch algorithm {
    case .SHA1:   return kAlgorithmSHA1
    case .SHA256: return kAlgorithmSHA256
    case .SHA512: return kAlgorithmSHA512
    }
}

private func algorithmFromString(string: String) -> Generator.Algorithm? {
    if string == kAlgorithmSHA1 { return .SHA1 }
    if string == kAlgorithmSHA256 { return .SHA256 }
    if string == kAlgorithmSHA512 { return .SHA512 }
    return nil
}

private func urlForToken(name name: String, issuer: String, factor: Generator.Factor, algorithm: Generator.Algorithm, digits: Int) throws -> NSURL {
    let urlComponents = NSURLComponents()
    urlComponents.scheme = kOTPAuthScheme
    urlComponents.path = "/" + name

    var queryItems = [
        NSURLQueryItem(name: kQueryAlgorithmKey, value: stringForAlgorithm(algorithm)),
        NSURLQueryItem(name: kQueryDigitsKey, value: String(digits)),
        NSURLQueryItem(name: kQueryIssuerKey, value: issuer)
    ]

    switch factor {
    case .Timer(let period):
        urlComponents.host = kFactorTimerKey
        queryItems.append(NSURLQueryItem(name: kQueryPeriodKey, value: String(Int(period))))
    case .Counter(let counter):
        urlComponents.host = kFactorCounterKey
        queryItems.append(NSURLQueryItem(name: kQueryCounterKey, value: String(counter)))
    }

    urlComponents.queryItems = queryItems

    guard let url = urlComponents.URL else {
        throw SerializationError.URLGenerationFailure
    }
    return url
}

private func tokenFromURL(url: NSURL, secret externalSecret: NSData? = nil) -> Token? {
    guard url.scheme == kOTPAuthScheme else {
        return nil
    }

    var queryDictionary = Dictionary<String, String>()
    NSURLComponents(URL: url, resolvingAgainstBaseURL: false)?.queryItems?.forEach { item in
        queryDictionary[item.name] = item.value
    }

    let factorParser: (string: String) -> Generator.Factor? = { string in
        if string == kFactorCounterKey {
            if let counter: UInt64 = parse(queryDictionary[kQueryCounterKey],
                with: {
                    errno = 0
                    let counterValue = strtoull(($0 as NSString).UTF8String, nil, 10)
                    guard errno == 0 else {
                        return nil
                    }
                    return counterValue
                },
                defaultTo: defaultCounter) {<commit_msg>Fix a bug when parsing a URL string with a non-integer counter<commit_after>//
//  Token+URL.swift
//  OneTimePassword
//
//  Copyright (c) 2014-2016 <NAME> and the OneTimePassword authors
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//

import Foundation
import Base32

extension Token {
    // MARK: Serialization

    /// Serializes the token to a URL.
    public func toURL() throws -> NSURL {
        return try urlForToken(
            name: name,
            issuer: issuer,
            factor: generator.factor,
            algorithm: generator.algorithm,
            digits: generator.digits
        )
    }

    /// Attempts to initialize a token represented by the give URL.
    public init?(url: NSURL, secret: NSData? = nil) {
        if let token = tokenFromURL(url, secret: secret) {
            self = token
        } else {
            return nil
        }
    }
}

internal enum SerializationError: ErrorType {
    case URLGenerationFailure
}

private let defaultAlgorithm: Generator.Algorithm = .SHA1
private let defaultDigits: Int = 6
private let defaultCounter: UInt64 = 0
private let defaultPeriod: NSTimeInterval = 30

private let kOTPAuthScheme = "otpauth"
private let kQueryAlgorithmKey = "algorithm"
private let kQuerySecretKey = "secret"
private let kQueryCounterKey = "counter"
private let kQueryDigitsKey = "digits"
private let kQueryPeriodKey = "period"
private let kQueryIssuerKey = "issuer"

private let kFactorCounterKey = "hotp"
private let kFactorTimerKey = "totp"

private let kAlgorithmSHA1   = "SHA1"
private let kAlgorithmSHA256 = "SHA256"
private let kAlgorithmSHA512 = "SHA512"

private func stringForAlgorithm(algorithm: Generator.Algorithm) -> String {
    switch algorithm {
    case .SHA1:   return kAlgorithmSHA1
    case .SHA256: return kAlgorithmSHA256
    case .SHA512: return kAlgorithmSHA512
    }
}

private func algorithmFromString(string: String) -> Generator.Algorithm? {
    if string == kAlgorithmSHA1 { return .SHA1 }
    if string == kAlgorithmSHA256 { return .SHA256 }
    if string == kAlgorithmSHA512 { return .SHA512 }
    return nil
}

private func urlForToken(name name: String, issuer: String, factor: Generator.Factor, algorithm: Generator.Algorithm, digits: Int) throws -> NSURL {
    let urlComponents = NSURLComponents()
    urlComponents.scheme = kOTPAuthScheme
    urlComponents.path = "/" + name

    var queryItems = [
        NSURLQueryItem(name: kQueryAlgorithmKey, value: stringForAlgorithm(algorithm)),
        NSURLQueryItem(name: kQueryDigitsKey, value: String(digits)),
        NSURLQueryItem(name: kQueryIssuerKey, value: issuer)
    ]

    switch factor {
    case .Timer(let period):
        urlComponents.host = kFactorTimerKey
        queryItems.append(NSURLQueryItem(name: kQueryPeriodKey, value: String(Int(period))))
    case .Counter(let counter):
        urlComponents.host = kFactorCounterKey
        queryItems.append(NSURLQueryItem(name: kQueryCounterKey, value: String(counter)))
    }

    urlComponents.queryItems = queryItems

    guard let url = urlComponents.URL else {
        throw SerializationError.URLGenerationFailure
    }
    return url
}

private func tokenFromURL(url: NSURL, secret externalSecret: NSData? = nil) -> Token? {
    guard url.scheme == kOTPAuthScheme else {
        return nil
    }

    var queryDictionary = Dictionary<String, String>()
    NSURLComponents(URL: url, resolvingAgainstBaseURL: false)?.queryItems?.forEach { item in
        queryDictionary[item.name] = item.value
    }

    let factorParser: (string: String) -> Generator.Factor? = { string in
        if string == kFactorCounterKey {
            if let counter: UInt64 = parse(queryDictionary[kQueryCounterKey],
                with: {
                    guard let counterValue = UInt64($0, radix: 10) else {
                        return nil
                    }
                    return counterValue
                },
                defaultTo: defaultCounter) {
{'fraction_non_alphanumeric': 0.08029051835638244, 'fraction_numerical': 0.013232514177693762, 'mean_word_length': 2.985725614591594, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 19, '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': '775797', 'n_tokens_mistral': 2810, 'n_tokens_neox': 2664, 'n_words': 957}