<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import Shared
import Deferred

@_exported import MozillaAppServices

private let log = Logger.syncLogger

public extension LoginRecord {
    public convenience init(credentials: URLCredential, protectionSpace: URLProtectionSpace) {
        let hostname: String
        if let _ = protectionSpace.`protocol` {
            hostname = protectionSpace.urlString()
        } else {
            hostname = protectionSpace.host
        }

        let httpRealm = protectionSpace.realm
        let username = credentials.user
        let password = <PASSWORD>

        self.init(fromJSONDict: [
            "hostname": hostname,
            "httpRealm": httpRealm as Any,
            "username": username ?? "",
            "password": password ?? ""
        ])
    }

    public var credentials: URLCredential {
        return URLCredential(user: username ?? "", password: password, persistence: .none)
    }

    public var protectionSpace: URLProtectionSpace {
        return URLProtectionSpace.fromOrigin(hostname)
    }

    public var hasMalformedHostname: Bool {
        let hostnameURL = hostname.asURL
        guard let _ = hostnameURL?.host else {
            return true
        }

        return false
    }

    public var isValid: Maybe<()> {
        // Referenced from https://mxr.mozilla.org/mozilla-central/source/toolkit/components/passwordmgr/nsLoginManager.js?rev=f76692f0fcf8&mark=280-281#271

        // Logins with empty hostnames are not valid.
        if hostname.isEmpty {
            return Maybe(failure: LoginRecordError(description: "Can't add a login with an empty hostname."))
        }

        // Logins with empty passwords are not valid.
        if password.isEmpty {
            return Maybe(failure: LoginRecordError(description: "Can't add a login with an empty password."))
        }

        // Logins with both a formSubmitURL and httpRealm are not valid.
        if let _ = formSubmitURL, let _ = httpRealm {
            return Maybe(failure: LoginRecordError(description: "Can't add a login with both a httpRealm and formSubmitURL."))
        }

        // Login must have at least a formSubmitURL or httpRealm.
        if (formSubmitURL == nil) && (httpRealm == nil) {
            return Maybe(failure: LoginRecordError(description: "Can't add a login without a httpRealm or formSubmitURL."))
        }

        // All good.
        return Maybe(success: ())
    }
}

public class LoginRecordError: MaybeErrorType {
    public let description: String
    public init(description: String) {
        self.description = description
    }
}

public class RustLogins {
    let databasePath: String
    let encryptionKey: String

    let queue: DispatchQueue
    let storage: LoginsStorage

    public fileprivate(set) var isOpen: Bool = false

    private var didAttemptToMoveToBackup = false

    public init(databasePath: String, encryptionKey: String) {
        self.databasePath = databasePath
        self.encryptionKey = encryptionKey

        self.queue =  DispatchQueue(label: "RustLogins queue: \(databasePath)", attributes: [])
        self.storage = LoginsStorage(databasePath: databasePath)
    }

    private func moveDatabaseFileToBackupLocation() {
        let databaseURL = URL(fileURLWithPath: databasePath)
        let databaseContainingDirURL = databaseURL.deletingLastPathComponent()
        let baseFilename = databaseURL.lastPathComponent

        // Attempt to make a backup as long as the database file still exists.
        guard FileManager.default.fileExists(atPath: databasePath) else {
            // No backup was attempted since the database file did not exist.
            Sentry.shared.sendWithStacktrace(message: "The Logins database was deleted while in use", tag: SentryTag.rustLogins)
            return
        }

        Sentry.shared.sendWithStacktrace(message: "Unable to open Logins database", tag: SentryTag.rustLogins, severity: .warning, description: "Attempting to move '\(baseFilename)'")

        // Note that a backup file might already exist! We append a counter to avoid this.
        var bakCounter = 0
        var bakBaseFilename: String
        var bakDatabasePath: String
        repeat {
            bakCounter += 1
            bakBaseFilename = "\(baseFilename).bak.\(bakCounter)"
            bakDatabasePath = databaseContainingDirURL.appendingPathComponent(bakBaseFilename).path
        } while FileManager.default.fileExists(atPath: bakDatabasePath)

        do {
            try FileManager.default.moveItem(atPath: bakDatabasePath, toPath: bakDatabasePath)

            let shmBaseFilename = baseFilename + "-shm"
            let walBaseFilename = baseFilename + "-wal"<commit_msg>Bug 1538289 - Not recovering from un-openable logins.db correctly (#4643)<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import Shared
import Deferred

@_exported import MozillaAppServices

private let log = Logger.syncLogger

public extension LoginRecord {
    public convenience init(credentials: URLCredential, protectionSpace: URLProtectionSpace) {
        let hostname: String
        if let _ = protectionSpace.`protocol` {
            hostname = protectionSpace.urlString()
        } else {
            hostname = protectionSpace.host
        }

        let httpRealm = protectionSpace.realm
        let username = credentials.user
        let password = <PASSWORD>

        self.init(fromJSONDict: [
            "hostname": hostname,
            "httpRealm": httpRealm as Any,
            "username": username ?? "",
            "password": password ?? ""
        ])
    }

    public var credentials: URLCredential {
        return URLCredential(user: username ?? "", password: password, persistence: .none)
    }

    public var protectionSpace: URLProtectionSpace {
        return URLProtectionSpace.fromOrigin(hostname)
    }

    public var hasMalformedHostname: Bool {
        let hostnameURL = hostname.asURL
        guard let _ = hostnameURL?.host else {
            return true
        }

        return false
    }

    public var isValid: Maybe<()> {
        // Referenced from https://mxr.mozilla.org/mozilla-central/source/toolkit/components/passwordmgr/nsLoginManager.js?rev=f76692f0fcf8&mark=280-281#271

        // Logins with empty hostnames are not valid.
        if hostname.isEmpty {
            return Maybe(failure: LoginRecordError(description: "Can't add a login with an empty hostname."))
        }

        // Logins with empty passwords are not valid.
        if password.isEmpty {
            return Maybe(failure: LoginRecordError(description: "Can't add a login with an empty password."))
        }

        // Logins with both a formSubmitURL and httpRealm are not valid.
        if let _ = formSubmitURL, let _ = httpRealm {
            return Maybe(failure: LoginRecordError(description: "Can't add a login with both a httpRealm and formSubmitURL."))
        }

        // Login must have at least a formSubmitURL or httpRealm.
        if (formSubmitURL == nil) && (httpRealm == nil) {
            return Maybe(failure: LoginRecordError(description: "Can't add a login without a httpRealm or formSubmitURL."))
        }

        // All good.
        return Maybe(success: ())
    }
}

public class LoginRecordError: MaybeErrorType {
    public let description: String
    public init(description: String) {
        self.description = description
    }
}

public class RustLogins {
    let databasePath: String
    let encryptionKey: String

    let queue: DispatchQueue
    let storage: LoginsStorage

    public fileprivate(set) var isOpen: Bool = false

    private var didAttemptToMoveToBackup = false

    public init(databasePath: String, encryptionKey: String) {
        self.databasePath = databasePath
        self.encryptionKey = encryptionKey

        self.queue =  DispatchQueue(label: "RustLogins queue: \(databasePath)", attributes: [])
        self.storage = LoginsStorage(databasePath: databasePath)
    }

    private func moveDatabaseFileToBackupLocation() {
        let databaseURL = URL(fileURLWithPath: databasePath)
        let databaseContainingDirURL = databaseURL.deletingLastPathComponent()
        let baseFilename = databaseURL.lastPathComponent

        // Attempt to make a backup as long as the database file still exists.
        guard FileManager.default.fileExists(atPath: databasePath) else {
            // No backup was attempted since the database file did not exist.
            Sentry.shared.sendWithStacktrace(message: "The Logins database was deleted while in use", tag: SentryTag.rustLogins)
            return
        }

        Sentry.shared.sendWithStacktrace(message: "Unable to open Logins database", tag: SentryTag.rustLogins, severity: .warning, description: "Attempting to move '\(baseFilename)'")

        // Note that a backup file might already exist! We append a counter to avoid this.
        var bakCounter = 0
        var bakBaseFilename: String
        var bakDatabasePath: String
        repeat {
            bakCounter += 1
            bakBaseFilename = "\(baseFilename).bak.\(bakCounter)"
            bakDatabasePath = databaseContainingDirURL.appendingPathComponent(bakBaseFilename).path
        } while FileManager.default.fileExists(atPath: bakDatabasePath)

        do {
            try FileManager.default.moveItem(atPath: databasePath, toPath: bakDatabasePath)

            let shmBaseFilename = baseFilename + "-shm"
            let walBaseFilename = baseFilename + "-wal"
{'fraction_non_alphanumeric': 0.07910976226605969, 'fraction_numerical': 0.00556398583712696, 'mean_word_length': 2.6601258793039615, 'pattern_counts': {'":': 8, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 2, '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': '4527031', 'n_tokens_mistral': 2661, 'n_tokens_neox': 2534, 'n_words': 801}