143 lines
5.9 KiB
Swift
143 lines
5.9 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import GRDB
|
|
|
|
public final class DatabaseCorruptionState: Codable, Equatable, CustomStringConvertible {
|
|
public enum DatabaseCorruptionStatus: Int, Codable, CustomStringConvertible {
|
|
// We used to store these as booleans, so the value is important.
|
|
case notCorrupted = 0
|
|
case corrupted = 1
|
|
case corruptedButAlreadyDumpedAndRestored = 2
|
|
case readCorrupted = 3
|
|
|
|
public var isCorrupted: Bool {
|
|
switch self {
|
|
case .notCorrupted, .readCorrupted: return false
|
|
case .corrupted, .corruptedButAlreadyDumpedAndRestored: return true
|
|
}
|
|
}
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .notCorrupted:
|
|
return "not corrupted"
|
|
case .readCorrupted:
|
|
return "read corrupted"
|
|
case .corrupted:
|
|
return "corrupted"
|
|
case .corruptedButAlreadyDumpedAndRestored:
|
|
return "corrupted (but already dumped and restored)"
|
|
}
|
|
}
|
|
}
|
|
|
|
public let status: DatabaseCorruptionStatus
|
|
public let count: UInt
|
|
|
|
init(status: DatabaseCorruptionStatus, count: UInt) {
|
|
self.status = status
|
|
self.count = count
|
|
}
|
|
|
|
public static func == (lhs: DatabaseCorruptionState, rhs: DatabaseCorruptionState) -> Bool {
|
|
(lhs.status == rhs.status) && (lhs.count == rhs.count)
|
|
}
|
|
|
|
public var description: String {
|
|
"Database is \(status). Corruption count: \(count)"
|
|
}
|
|
|
|
// MARK: - Reading and writing from `UserDefaults`
|
|
|
|
// The value of this key doesn't match the name because that's what we used to store.
|
|
static var databaseCorruptionStatusKey: String { "hasGrdbDatabaseCorruption" }
|
|
static var databaseCorruptionCountKey: String { "databaseCorruptionCount" }
|
|
|
|
public convenience init(userDefaults: UserDefaults) {
|
|
let rawStatus = userDefaults.integer(forKey: Self.databaseCorruptionStatusKey)
|
|
let rawCount = userDefaults.integer(forKey: Self.databaseCorruptionCountKey)
|
|
|
|
let status = DatabaseCorruptionStatus(rawValue: rawStatus) ?? .notCorrupted
|
|
let count: UInt = status.isCorrupted ? max(UInt(rawCount), 1) : UInt(rawCount)
|
|
|
|
self.init(status: status, count: count)
|
|
}
|
|
|
|
private func save(to userDefaults: UserDefaults) {
|
|
userDefaults.set(status.rawValue, forKey: Self.databaseCorruptionStatusKey)
|
|
userDefaults.set(count, forKey: Self.databaseCorruptionCountKey)
|
|
}
|
|
|
|
/// If the error is a `SQLITE_CORRUPT` error, set the "has database corruption" flag, log, and crash.
|
|
/// We do this so we can attempt to perform diagnostics/recovery on relaunch.
|
|
public static func flagDatabaseCorruptionIfNecessary(
|
|
userDefaults: UserDefaults,
|
|
error: Error,
|
|
file: String = #fileID,
|
|
function: String = #function,
|
|
line: Int = #line
|
|
) {
|
|
if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT {
|
|
flagDatabaseAsCorrupted(userDefaults: userDefaults)
|
|
owsFail("Crashing due to database corruption. Extended result code: \(error.extendedResultCode)", file: file, function: function, line: line)
|
|
}
|
|
}
|
|
|
|
/// If the error is a `SQLITE_CORRUPT` error, set the "has database read corruption" flag, log, but don't crash.
|
|
/// We do this so we can attempt to perform diagnostics/recovery if this read error is coupled with a crash..
|
|
public static func flagDatabaseReadCorruptionIfNecessary(userDefaults: UserDefaults, error: Error) {
|
|
if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT {
|
|
flagDatabaseAsReadCorrupted(userDefaults: userDefaults)
|
|
}
|
|
}
|
|
|
|
public static func flagDatabaseAsCorrupted(userDefaults: UserDefaults) {
|
|
let oldState = DatabaseCorruptionState(userDefaults: userDefaults)
|
|
switch oldState.status {
|
|
case .notCorrupted, .readCorrupted:
|
|
Self(status: .corrupted, count: oldState.count + 1).save(to: userDefaults)
|
|
case .corrupted, .corruptedButAlreadyDumpedAndRestored:
|
|
return
|
|
}
|
|
}
|
|
|
|
public static func flagDatabaseAsReadCorrupted(userDefaults: UserDefaults) {
|
|
let oldState = DatabaseCorruptionState(userDefaults: userDefaults)
|
|
switch oldState.status {
|
|
case .notCorrupted:
|
|
Self(status: .readCorrupted, count: oldState.count).save(to: userDefaults)
|
|
case .corrupted, .readCorrupted, .corruptedButAlreadyDumpedAndRestored:
|
|
return
|
|
}
|
|
}
|
|
|
|
public static func flagCorruptedDatabaseAsDumpedAndRestored(userDefaults: UserDefaults) {
|
|
let oldState = DatabaseCorruptionState(userDefaults: userDefaults)
|
|
switch oldState.status {
|
|
case .corrupted, .readCorrupted:
|
|
DatabaseCorruptionState(status: .corruptedButAlreadyDumpedAndRestored, count: oldState.count).save(to: userDefaults)
|
|
case .notCorrupted, .corruptedButAlreadyDumpedAndRestored:
|
|
owsFailDebug("Flagging database as partially recovered, but it was not in the right state previously")
|
|
}
|
|
}
|
|
|
|
public static func flagDatabaseAsRecoveredFromCorruption(userDefaults: UserDefaults) {
|
|
let oldState = DatabaseCorruptionState(userDefaults: userDefaults)
|
|
switch oldState.status {
|
|
case .notCorrupted:
|
|
owsFailDebug("Flagging database as recovered from corruption, but it wasn't marked corrupted")
|
|
case .corrupted, .readCorrupted, .corruptedButAlreadyDumpedAndRestored:
|
|
Self(status: .notCorrupted, count: oldState.count).save(to: userDefaults)
|
|
}
|
|
}
|
|
|
|
@objc(stringForLoggingWith:)
|
|
public static func objcStringForLogging(userDefaults: UserDefaults) -> String {
|
|
let state = DatabaseCorruptionState(userDefaults: userDefaults)
|
|
return String(describing: state)
|
|
}
|
|
}
|