1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 // A lot of stuff in this class was originally written in objective-c, and the swift 4 // equivalents don't seem to work quite the same, which is important to have backwards 5 // data compatibility. 6 // swiftlint:disable legacy_objc_type 7 // swiftlint:disable force_cast 8 // swiftlint:disable force_unwrapping 9 10 import Foundation 11 import sqlite3 12 13 internal struct UpdatesDatabaseUtilsErrorInfo { 14 let code: Int 15 let extendedCode: Int 16 let message: String 17 } 18 19 internal struct UpdatesDatabaseUtilsError: Error { 20 enum ErrorKind { 21 case SQLitePrepareError 22 case SQLiteArgsBindError 23 case SQLiteBlobNotUUID 24 case SQLiteGetResultsError 25 } 26 27 let kind: ErrorKind 28 let info: UpdatesDatabaseUtilsErrorInfo? 29 } 30 31 // these are not exported in the swift headers 32 let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self) 33 let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) 34 35 private extension UUID { 36 var data: Data { 37 return withUnsafeBytes(of: self.uuid, { Data($0) }) 38 } 39 } 40 41 /** 42 * Utility class with methods for common database functions used across multiple classes. 43 */ 44 internal final class UpdatesDatabaseUtils { executenull45 static func execute(sql: String, withArgs args: [Any?]?, onDatabase db: OpaquePointer) throws -> [[String: Any?]] { 46 var stmt: OpaquePointer? 47 guard sqlite3_prepare_v2(db, String(sql.utf8), -1, &stmt, nil) == SQLITE_OK, 48 let stmt = stmt else { 49 throw UpdatesDatabaseUtilsError(kind: .SQLitePrepareError, info: errorCodesAndMessage(fromSqlite: db)) 50 } 51 52 if let args = args { 53 guard bind(statement: stmt, withArgs: args) else { 54 throw UpdatesDatabaseUtilsError(kind: .SQLiteArgsBindError, info: errorCodesAndMessage(fromSqlite: db)) 55 } 56 } 57 58 var rows: [[String: Any?]] = [] 59 var columnNames: [String] = [] 60 61 var columnCount: Int32 = 0 62 var didFetchColumns = false 63 var result: Int32 64 var hasMore = true 65 var didError = false 66 67 while hasMore { 68 result = sqlite3_step(stmt) 69 switch result { 70 case SQLITE_ROW: 71 if !didFetchColumns { 72 // get all column names once at the beginning 73 columnCount = sqlite3_column_count(stmt) 74 75 for i in 0..<columnCount { 76 columnNames.append(String(utf8String: sqlite3_column_name(stmt, Int32(i)))!) 77 } 78 79 didFetchColumns = true 80 } 81 82 var entry: [String: Any] = [:] 83 for i in 0..<columnCount { 84 let columnValue = try getValue(withStatement: stmt, column: i) 85 entry[columnNames[Int(i)]] = columnValue 86 } 87 rows.append(entry) 88 case SQLITE_DONE: 89 hasMore = false 90 default: 91 didError = true 92 hasMore = false 93 } 94 } 95 96 sqlite3_finalize(stmt) 97 98 if didError { 99 throw UpdatesDatabaseUtilsError(kind: .SQLiteGetResultsError, info: errorCodesAndMessage(fromSqlite: db)) 100 } 101 102 return rows 103 } 104 bindnull105 private static func bind(statement stmt: OpaquePointer, withArgs args: [Any?]) -> Bool { 106 for (index, arg) in args.enumerated() { 107 let bindIdx = Int32(index + 1) 108 switch arg { 109 case let arg as UUID: 110 guard withUnsafeBytes(of: arg.uuid, { bufferPointer -> Int32 in 111 sqlite3_bind_blob(stmt, bindIdx, bufferPointer.baseAddress, 16, SQLITE_TRANSIENT) 112 }) == SQLITE_OK else { 113 return false 114 } 115 case let arg as NSNumber: 116 guard sqlite3_bind_int64(stmt, bindIdx, arg.int64Value) == SQLITE_OK else { 117 return false 118 } 119 case let arg as Date: 120 let dateValue = arg.timeIntervalSince1970 * 1000 121 guard sqlite3_bind_int64(stmt, bindIdx, Int64(dateValue)) == SQLITE_OK else { 122 return false 123 } 124 case let arg as NSDictionary: 125 guard let jsonData = try? JSONSerialization.data(withJSONObject: arg) as NSData else { 126 return false 127 } 128 guard sqlite3_bind_text(stmt, bindIdx, jsonData.bytes, Int32(jsonData.length), SQLITE_TRANSIENT) == SQLITE_OK else { 129 return false 130 } 131 case nil: 132 guard sqlite3_bind_null(stmt, bindIdx) == SQLITE_OK else { 133 return false 134 } 135 default: 136 // convert to string 137 var string: NSString 138 if let argNSString = arg as? NSString { 139 string = argNSString 140 } else { 141 string = (arg as! NSObject).description as NSString 142 } 143 let data = string.data(using: NSUTF8StringEncoding)! as NSData 144 guard sqlite3_bind_text(stmt, bindIdx, data.bytes, Int32(data.length), SQLITE_TRANSIENT) == SQLITE_OK else { 145 return false 146 } 147 } 148 } 149 return true 150 } 151 getValuenull152 private static func getValue(withStatement stmt: OpaquePointer, column: Int32) throws -> Any? { 153 let columnType = sqlite3_column_type(stmt, column) 154 switch columnType { 155 case SQLITE_INTEGER: 156 return sqlite3_column_int64(stmt, column) 157 case SQLITE_FLOAT: 158 return sqlite3_column_double(stmt, column) 159 case SQLITE_BLOB: 160 guard sqlite3_column_bytes(stmt, column) == 16 else { 161 throw UpdatesDatabaseUtilsError(kind: .SQLiteBlobNotUUID, info: nil) 162 } 163 let blob = Data(bytes: sqlite3_column_blob(stmt, column), count: 16) 164 return blob.withUnsafeBytes { rawBytes -> UUID in 165 NSUUID(uuidBytes: rawBytes) as UUID 166 } 167 case SQLITE_TEXT: 168 return NSString( 169 bytes: sqlite3_column_text(stmt, column), 170 length: Int(sqlite3_column_bytes(stmt, column)), 171 encoding: NSUTF8StringEncoding 172 ) as? String 173 default: 174 return nil 175 } 176 } 177 errorCodesAndMessagenull178 static func errorCodesAndMessage(fromSqlite db: OpaquePointer) -> UpdatesDatabaseUtilsErrorInfo { 179 let code = sqlite3_errcode(db) 180 let extendedCode = sqlite3_extended_errcode(db) 181 let message = String(cString: sqlite3_errmsg(db)) 182 return UpdatesDatabaseUtilsErrorInfo(code: Int(code), extendedCode: Int(extendedCode), message: message) 183 } 184 datenull185 static func date(fromUnixTimeMilliseconds number: NSNumber) -> Date { 186 return Date(timeIntervalSince1970: number.doubleValue / 1000) 187 } 188 } 189