// // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import GRDB import SignalCoreKit // This class can be used to: // // * Back a preferences class. // * To persist simple values in our managers. // * Etc. @objc public class SDSKeyValueStore: NSObject { // Key-value stores use "collections" to group related keys. // // * In GRDB, we store each model in a separate table but // all k-v stores are in a single table. // GRDB maintains a mapping between tables and collections. // For the purposes of this mapping only we use dataStoreCollection. static let dataStoreCollection = "keyvalue" static let tableName = "keyvalue" // By default, all reads/writes use this collection. @objc public let collection: String static let collectionColumn = SDSColumnMetadata(columnName: "collection", columnType: .unicodeString, isOptional: false) static let keyColumn = SDSColumnMetadata(columnName: "key", columnType: .unicodeString, isOptional: false) static let valueColumn = SDSColumnMetadata(columnName: "value", columnType: .blob, isOptional: false) public static let table = SDSTableMetadata( collection: SDSKeyValueStore.dataStoreCollection, tableName: SDSKeyValueStore.tableName, columns: [ collectionColumn, keyColumn, valueColumn ] ) @objc public required init(collection: String) { self.collection = collection super.init() } @objc public class func logCollectionStatistics() { Logger.info("SDSKeyValueStore statistics:") databaseStorage.read { transaction in do { let sql = """ SELECT \(collectionColumn.columnName), COUNT(*) FROM \(table.tableName) GROUP BY \(collectionColumn.columnName) ORDER BY COUNT(*) DESC LIMIT 10 """ let cursor = try Row.fetchCursor(transaction.unwrapGrdbRead.database, sql: sql) while let row = try cursor.next() { let collection: String = row[0] let count: UInt = row[1] Logger.info("- \(collection): \(count) items") } } catch { Logger.error("\(error)") } } } // MARK: Class Helpers @objc public class func key(int: Int) -> String { return NSNumber(value: int).stringValue } @objc public func hasValue(forKey key: String, transaction: SDSAnyReadTransaction) -> Bool { switch transaction.readTransaction { case .grdbRead(let grdbTransaction): do { let count = try UInt.fetchOne(grdbTransaction.database, sql: """ SELECT COUNT(*) FROM \(SDSKeyValueStore.table.tableName) WHERE \(SDSKeyValueStore.keyColumn.columnName) = ? AND \(SDSKeyValueStore.collectionColumn.columnName) == ? """, arguments: [key, collection]) ?? 0 return count > 0 } catch { owsFailDebug("error: \(error)") return false } } } // MARK: - String @objc public func getString(_ key: String, transaction: SDSAnyReadTransaction) -> String? { return read(key, transaction: transaction) } @objc public func setString(_ value: String?, key: String, transaction: SDSAnyWriteTransaction) { guard let value = value else { write(nil, forKey: key, transaction: transaction) return } write(value as NSString, forKey: key, transaction: transaction) } // MARK: - Date @objc public func getDate(_ key: String, transaction: SDSAnyReadTransaction) -> Date? { // Our legacy methods sometimes stored dates as NSNumber and // sometimes as NSDate, so we are permissive when decoding. guard let object: NSObject = read(key, transaction: transaction) else { return nil } if let date = object as? Date { return date } guard let epochInterval = object as? NSNumber else { owsFailDebug("Could not decode value: \(type(of: object)).") return nil } return Date(timeIntervalSince1970: epochInterval.doubleValue) } @objc public func setDate(_ value: Date, key: String, transaction: SDSAnyWriteTransaction) { let epochInterval = NSNumber(value: value.timeIntervalSince1970) setObject(epochInterval, key: key, transaction: transaction) } // MARK: - Bool public func getBool(_ key: String, transaction: SDSAnyReadTransaction) -> Bool? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.boolValue } @objc public func getBool(_ key: String, defaultValue: Bool, transaction: SDSAnyReadTransaction) -> Bool { return getBool(key, transaction: transaction) ?? defaultValue } @objc public func setBool(_ value: Bool, key: String, transaction: SDSAnyWriteTransaction) { write(NSNumber(value: value), forKey: key, transaction: transaction) } @objc public func setBoolIfChanged(_ value: Bool, defaultValue: Bool, key: String, transaction: SDSAnyWriteTransaction) { let didChange = value != getBool(key, defaultValue: defaultValue, transaction: transaction) guard didChange else { return } setBool(value, key: key, transaction: transaction) } // MARK: - UInt public func getUInt(_ key: String, transaction: SDSAnyReadTransaction) -> UInt? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.uintValue } // TODO: Handle numerics more generally. @objc public func getUInt(_ key: String, defaultValue: UInt, transaction: SDSAnyReadTransaction) -> UInt { return getUInt(key, transaction: transaction) ?? defaultValue } @objc public func setUInt(_ value: UInt, key: String, transaction: SDSAnyWriteTransaction) { write(NSNumber(value: value), forKey: key, transaction: transaction) } // MARK: - Data @objc public func getData(_ key: String, transaction: SDSAnyReadTransaction) -> Data? { return readData(key, transaction: transaction) } @objc public func setData(_ value: Data?, key: String, transaction: SDSAnyWriteTransaction) { writeData(value, forKey: key, transaction: transaction) } // MARK: - Numeric @objc public func getNSNumber(_ key: String, transaction: SDSAnyReadTransaction) -> NSNumber? { let number: NSNumber? = read(key, transaction: transaction) return number } // MARK: - Int public func getInt(_ key: String, transaction: SDSAnyReadTransaction) -> Int? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.intValue } @objc public func getInt(_ key: String, defaultValue: Int, transaction: SDSAnyReadTransaction) -> Int { return getInt(key, transaction: transaction) ?? defaultValue } @objc public func setInt(_ value: Int, key: String, transaction: SDSAnyWriteTransaction) { setObject(NSNumber(value: value), key: key, transaction: transaction) } // MARK: - Int32 public func getInt32(_ key: String, transaction: SDSAnyReadTransaction) -> Int32? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.int32Value } @objc public func getInt32(_ key: String, defaultValue: Int32, transaction: SDSAnyReadTransaction) -> Int32 { return getInt32(key, transaction: transaction) ?? defaultValue } @objc public func setInt32(_ value: Int32, key: String, transaction: SDSAnyWriteTransaction) { setObject(NSNumber(value: value), key: key, transaction: transaction) } // MARK: - UInt32 public func getUInt32(_ key: String, transaction: SDSAnyReadTransaction) -> UInt32? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.uint32Value } @objc public func getUInt32(_ key: String, defaultValue: UInt32, transaction: SDSAnyReadTransaction) -> UInt32 { return getUInt32(key, transaction: transaction) ?? defaultValue } @objc public func setUInt32(_ value: UInt32, key: String, transaction: SDSAnyWriteTransaction) { setObject(NSNumber(value: value), key: key, transaction: transaction) } // MARK: - UInt64 public func getUInt64(_ key: String, transaction: SDSAnyReadTransaction) -> UInt64? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.uint64Value } @objc public func getUInt64(_ key: String, defaultValue: UInt64, transaction: SDSAnyReadTransaction) -> UInt64 { return getUInt64(key, transaction: transaction) ?? defaultValue } @objc public func setUInt64(_ value: UInt64, key: String, transaction: SDSAnyWriteTransaction) { setObject(NSNumber(value: value), key: key, transaction: transaction) } // MARK: - Int64 public func getInt64(_ key: String, transaction: SDSAnyReadTransaction) -> Int64? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.int64Value } @objc public func getInt64(_ key: String, defaultValue: Int64, transaction: SDSAnyReadTransaction) -> Int64 { return getInt64(key, transaction: transaction) ?? defaultValue } @objc public func setInt64(_ value: Int64, key: String, transaction: SDSAnyWriteTransaction) { setObject(NSNumber(value: value), key: key, transaction: transaction) } // MARK: - Double public func getDouble(_ key: String, transaction: SDSAnyReadTransaction) -> Double? { guard let number: NSNumber = read(key, transaction: transaction) else { return nil } return number.doubleValue } @objc public func getDouble(_ key: String, defaultValue: Double, transaction: SDSAnyReadTransaction) -> Double { return getDouble(key, transaction: transaction) ?? defaultValue } @objc public func setDouble(_ value: Double, key: String, transaction: SDSAnyWriteTransaction) { setObject(NSNumber(value: value), key: key, transaction: transaction) } // MARK: - Object @objc public func getObject(forKey key: String, transaction: SDSAnyReadTransaction) -> Any? { return read(key, transaction: transaction) } @objc public func setObject(_ anyValue: Any?, key: String, transaction: SDSAnyWriteTransaction) { guard let anyValue = anyValue else { write(nil, forKey: key, transaction: transaction) return } guard let codingValue = anyValue as? NSCoding else { owsFailDebug("Invalid value.") write(nil, forKey: key, transaction: transaction) return } write(codingValue, forKey: key, transaction: transaction) } // MARK: - @objc public func removeValue(forKey key: String, transaction: SDSAnyWriteTransaction) { write(nil, forKey: key, transaction: transaction) } @objc public func removeValues(forKeys keys: [String], transaction: SDSAnyWriteTransaction) { for key in keys { write(nil, forKey: key, transaction: transaction) } } @objc public func removeAll(transaction: SDSAnyWriteTransaction) { switch transaction.writeTransaction { case .grdbWrite(let grdbWrite): let sql = """ DELETE FROM \(SDSKeyValueStore.table.tableName) WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ? """ grdbWrite.executeAndCacheStatement(sql: sql, arguments: [collection]) } } @objc public func enumerateKeysAndObjects(transaction: SDSAnyReadTransaction, block: (String, Any, UnsafeMutablePointer) -> Void) { switch transaction.readTransaction { case .grdbRead(let grdbRead): var stop: ObjCBool = false // PERF - we could enumerate with a single query rather than // fetching keys then fetching objects one by one. In practice // the collections that use this are pretty small. for key in allKeys(grdbTransaction: grdbRead) { guard !stop.boolValue else { return } guard let value: Any = read(key, transaction: transaction) else { owsFailDebug("value was unexpectedly nil") continue } block(key, value, &stop) } } } @objc public func enumerateKeys(transaction: SDSAnyReadTransaction, block: (String, UnsafeMutablePointer) -> Void) { switch transaction.readTransaction { case .grdbRead(let grdbRead): var stop: ObjCBool = false for key in allKeys(grdbTransaction: grdbRead) { guard !stop.boolValue else { return } block(key, &stop) } } } @objc public func allValues(transaction: SDSAnyReadTransaction) -> [Any] { return allKeys(transaction: transaction).compactMap { key in return self.read(key, transaction: transaction) } } @objc public func allDataValues(transaction: SDSAnyReadTransaction) -> [Data] { return allKeys(transaction: transaction).compactMap { key in return self.getData(key, transaction: transaction) } } private struct PairRecord: Codable, FetchableRecord, PersistableRecord { public let key: String? public let value: Data? } private func allPairs(transaction: SDSAnyReadTransaction) -> [PairRecord] { switch transaction.readTransaction { case .grdbRead(let grdbTransaction): let sql = """ SELECT \(SDSKeyValueStore.keyColumn.columnName), \(SDSKeyValueStore.valueColumn.columnName) FROM \(SDSKeyValueStore.table.tableName) WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ? """ do { return try PairRecord.fetchAll(grdbTransaction.database, sql: sql, arguments: [collection]) } catch { owsFailDebug("Error: \(error)") return [] } } } @objc public func allBoolValuesMap(transaction: SDSAnyReadTransaction) -> [String: Bool] { let pairs = allPairs(transaction: transaction) var result = [String: Bool]() for pair in pairs { guard let key = pair.key else { owsFailDebug("missing key.") continue } guard let value = pair.value else { owsFailDebug("missing value.") continue } guard let rawObject = parseArchivedValue(value) else { owsFailDebug("Could not parse value.") continue } guard let number: NSNumber = parseValueAs(key: key, rawObject: rawObject) else { owsFailDebug("Invalid value.") continue } result[key] = number.boolValue } return result } @objc public func allUIntValuesMap(transaction: SDSAnyReadTransaction) -> [String: UInt] { let pairs = allPairs(transaction: transaction) var result = [String: UInt]() for pair in pairs { guard let key = pair.key else { owsFailDebug("missing key.") continue } guard let value = pair.value else { owsFailDebug("missing value.") continue } guard let rawObject = parseArchivedValue(value) else { owsFailDebug("Could not parse value.") continue } guard let number: NSNumber = parseValueAs(key: key, rawObject: rawObject) else { owsFailDebug("Invalid value.") continue } result[key] = number.uintValue } return result } @objc public func anyDataValue(transaction: SDSAnyReadTransaction) -> Data? { let keys = allKeys(transaction: transaction).shuffled() guard let firstKey = keys.first else { return nil } guard let data = self.getData(firstKey, transaction: transaction) else { owsFailDebug("Missing data for key: \(firstKey)") return nil } return data } @objc public func allKeys(transaction: SDSAnyReadTransaction) -> [String] { switch transaction.readTransaction { case .grdbRead(let grdbRead): return allKeys(grdbTransaction: grdbRead) } } @objc public func numberOfKeys(transaction: SDSAnyReadTransaction) -> UInt { switch transaction.readTransaction { case .grdbRead(let grdbRead): let sql = """ SELECT COUNT(*) FROM \(SDSKeyValueStore.table.tableName) WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ? """ do { guard let numberOfKeys = try UInt.fetchOne(grdbRead.database, sql: sql, arguments: [collection]) else { throw OWSAssertionError("numberOfKeys was unexpectedly nil") } return numberOfKeys } catch { DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFail("error: \(error)") } } } @objc public var asObjC: SDSKeyValueStoreObjC { return SDSKeyValueStoreObjC(sdsKeyValueStore: self) } // MARK: - @available(*, deprecated, message: "Did you mean removeValue(forKey:transaction:) or setCodable(optional:key:transaction)?") public func setCodable(_ value: T?, key: String, transaction: SDSAnyWriteTransaction) throws { try setCodable(optional: value, key: key, transaction: transaction) } public func setCodable(_ value: T, key: String, transaction: SDSAnyWriteTransaction) throws { // The only difference between setCodable(optional:...) and setCodable(_...) is // the non-optional variant has a deprecated overload to warn callers of the ambiguity. try setCodable(optional: value, key: key, transaction: transaction) } public func setCodable(optional value: T, key: String, transaction: SDSAnyWriteTransaction) throws { do { let data = try JSONEncoder().encode(value) setData(data, key: key, transaction: transaction) } catch { owsFailDebug("Failed to encode: \(error).") throw error } } public func getCodableValue(forKey key: String, transaction: SDSAnyReadTransaction) throws -> T? { guard let data = getData(key, transaction: transaction) else { return nil } do { return try JSONDecoder().decode(T.self, from: data) } catch { owsFailDebug("Failed to decode: \(error).") throw error } } public func allCodableValues(transaction: SDSAnyReadTransaction) throws -> [T] { var result = [T]() for data in allDataValues(transaction: transaction) { do { result.append(try JSONDecoder().decode(T.self, from: data)) } catch { owsFailDebug("Failed to decode: \(error).") throw error } } return result } // MARK: - Internal Methods private func read(_ key: String, transaction: SDSAnyReadTransaction) -> T? { guard let rawObject = readRawObject(key, transaction: transaction) else { return nil } return parseValueAs(key: key, rawObject: rawObject) } private func readRawObject(_ key: String, transaction: SDSAnyReadTransaction) -> Any? { // GRDB values are serialized to data by this class. switch transaction.readTransaction { case .grdbRead: guard let encoded = readData(key, transaction: transaction) else { return nil } return parseArchivedValue(encoded) } } private func parseArchivedValue(_ encoded: Data) -> Any? { do { guard let rawObject = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encoded) else { owsFailDebug("Could not decode value.") return nil } return rawObject } catch { owsFailDebug("Decode failed.") return nil } } private func parseValueAs(key: String, rawObject: Any?) -> T? { guard let rawObject = rawObject else { return nil } guard let object = rawObject as? T else { owsFailDebug("Value for key: \(key) has unexpected type: \(type(of: rawObject)).") return nil } return object } private func readData(_ key: String, transaction: SDSAnyReadTransaction) -> Data? { let collection = self.collection switch transaction.readTransaction { case .grdbRead(let grdbTransaction): return SDSKeyValueStore.readData(transaction: grdbTransaction, key: key, collection: collection) } } private class func readData(transaction: GRDBReadTransaction, key: String, collection: String) -> Data? { do { return try Data.fetchOne(transaction.database, sql: "SELECT \(self.valueColumn.columnName) FROM \(self.table.tableName) WHERE \(self.keyColumn.columnName) = ? AND \(collectionColumn.columnName) == ?", arguments: [key, collection]) } catch { DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFailDebug("error: \(error)") return nil } } // TODO: Codable? NSCoding? Other serialization? private func write(_ value: NSCoding?, forKey key: String, transaction: SDSAnyWriteTransaction) { // GRDB values are serialized to data by this class. switch transaction.writeTransaction { case .grdbWrite: if let value = value { let encoded = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false) writeData(encoded, forKey: key, transaction: transaction) } else { writeData(nil, forKey: key, transaction: transaction) } } } private func writeData(_ data: Data?, forKey key: String, transaction: SDSAnyWriteTransaction) { let collection = self.collection switch transaction.writeTransaction { case .grdbWrite(let grdbTransaction): do { try SDSKeyValueStore.write(transaction: grdbTransaction, key: key, collection: collection, encoded: data) } catch { owsFailDebug("error: \(error)") } } } private class func write(transaction: GRDBWriteTransaction, key: String, collection: String, encoded: Data?) throws { if let encoded = encoded { // See: https://www.sqlite.org/lang_UPSERT.html let sql = """ INSERT INTO \(table.tableName) ( \(keyColumn.columnName), \(collectionColumn.columnName), \(valueColumn.columnName) ) VALUES (?, ?, ?) ON CONFLICT ( \(keyColumn.columnName), \(collectionColumn.columnName) ) DO UPDATE SET \(valueColumn.columnName) = ? """ try update(transaction: transaction, sql: sql, arguments: [ key, collection, encoded, encoded ]) } else { // Setting to nil is a delete. let sql = "DELETE FROM \(table.tableName) WHERE \(keyColumn.columnName) == ? AND \(collectionColumn.columnName) == ?" try update(transaction: transaction, sql: sql, arguments: [ key, collection ]) } } private class func update( transaction: GRDBWriteTransaction, sql: String, arguments: StatementArguments ) throws { let statement = try transaction.database.cachedStatement(sql: sql) try statement.setArguments(arguments) do { try statement.execute() } catch { DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) throw error } } private func allKeys(grdbTransaction: GRDBReadTransaction) -> [String] { let sql = """ SELECT \(SDSKeyValueStore.keyColumn.columnName) FROM \(SDSKeyValueStore.table.tableName) WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ? """ return grdbTransaction.database.strictRead { database in try String.fetchAll(database, sql: sql, arguments: [collection]) } } }