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