VPN v2
This commit is contained in:
276
main/DB/DBWrapper.swift
Normal file
276
main/DB/DBWrapper.swift
Normal file
@@ -0,0 +1,276 @@
|
||||
import UIKit
|
||||
|
||||
let DBWrp = DBWrapper()
|
||||
|
||||
class DBWrapper {
|
||||
private var latestModification: Timestamp = 0
|
||||
private var dataA: [GroupedDomain] = [] // Domains
|
||||
private var dataB: [[GroupedDomain]] = [] // Hosts
|
||||
private var dataF: [String : FilterOptions] = [:] // Filters
|
||||
private let Q = DispatchQueue(label: "de.uni-bamberg.psi.AppCheck.db-wrapper-queue", attributes: .concurrent)
|
||||
|
||||
// auto update rows callback
|
||||
var currentlyOpenParent: String?
|
||||
weak var dataA_delegate: IncrementalDataSourceUpdate?
|
||||
weak var dataB_delegate: IncrementalDataSourceUpdate?
|
||||
func dataB_delegate(_ parent: String) -> IncrementalDataSourceUpdate? {
|
||||
(currentlyOpenParent == parent) ? dataB_delegate : nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Data Source Getter
|
||||
|
||||
func listOfDomains() -> [GroupedDomain] {
|
||||
Q.sync() { dataA }
|
||||
}
|
||||
|
||||
func listOfHosts(_ parent: String) -> [GroupedDomain] {
|
||||
Q.sync() { dataB[ifExist: dataA_index(of: parent)] ?? [] }
|
||||
}
|
||||
|
||||
func dataF_list(_ filter: FilterOptions) -> [String] {
|
||||
Q.sync() { dataF.compactMap { $1.contains(filter) ? $0 : nil } }
|
||||
}
|
||||
|
||||
func dataF_counts() -> (blocked: Int, ignored: Int) {
|
||||
Q.sync() { dataF.reduce((0, 0)) {
|
||||
($0.0 + ($1.1.contains(.blocked) ? 1 : 0),
|
||||
$0.1 + ($1.1.contains(.ignored) ? 1 : 0)) }}
|
||||
}
|
||||
|
||||
func listOfTimes(_ domain: String?) -> [(Timestamp, Bool)] {
|
||||
guard let domain = domain else { return [] }
|
||||
return AppDB?.timesForDomain(domain)?.reversed() ?? []
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
func initContentOfDB() {
|
||||
DispatchQueue.global().async {
|
||||
#if IOS_SIMULATOR
|
||||
// self.generateTestData()
|
||||
// DispatchQueue.main.async {
|
||||
// // dont know why main queue is needed, wont start otherwise
|
||||
// Timer.repeating(2, call: #selector(self.insertRandomEntry), on: self)
|
||||
// }
|
||||
#endif
|
||||
self.dataF_init()
|
||||
self.dataAB_init()
|
||||
self.autoSyncTimer_init()
|
||||
}
|
||||
}
|
||||
|
||||
private func dataF_init() {
|
||||
let list = AppDB?.loadFilters() ?? [:]
|
||||
Q.async(flags: .barrier) {
|
||||
self.dataF = list
|
||||
NotifyFilterChanged.postOnMainThread()
|
||||
}
|
||||
}
|
||||
|
||||
private func dataAB_init() {
|
||||
let list = AppDB?.domainList()
|
||||
Q.async(flags: .barrier) {
|
||||
self.dataA = []
|
||||
self.dataB = []
|
||||
self.latestModification = 0
|
||||
if let allDomains = list {
|
||||
for (parent, parts) in self.groupBySubdomains(allDomains) {
|
||||
self.dataA.append(parent)
|
||||
self.dataB.append(parts)
|
||||
self.latestModification = max(parent.lastModified, self.latestModification)
|
||||
}
|
||||
}
|
||||
NotifyLogHistoryReset.postOnMainThread()
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto sync new logs every 7 seconds.
|
||||
private func autoSyncTimer_init() {
|
||||
Q.async() { // using Q to start timer only after init data A,B,F
|
||||
DispatchQueue.main.async {
|
||||
// dont know why main queue is needed, wont start otherwise
|
||||
Timer.repeating(7, call: #selector(self.syncNewestLogs), on: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Partial Update History
|
||||
|
||||
@objc private func syncNewestLogs() {
|
||||
QLog.Debug("\(#function)")
|
||||
#if !IOS_SIMULATOR
|
||||
guard currentVPNState == .on else { return }
|
||||
#endif
|
||||
guard let res = AppDB?.domainList(since: latestModification), res.count > 0 else {
|
||||
return
|
||||
}
|
||||
QLog.Info("auto sync \(res.count) new logs")
|
||||
Q.async(flags: .barrier) {
|
||||
var c = 0
|
||||
for (parent, parts) in self.groupBySubdomains(res) {
|
||||
if let i = self.dataA_index(of: parent.domain) {
|
||||
self.mergeExistingParts(parent.domain, at: i, newChildren: parts)
|
||||
|
||||
let merged = parent + self.dataA.remove(at: i)
|
||||
self.dataA.insert(merged, at: c)
|
||||
self.dataB.insert(self.dataB.remove(at: i), at: c)
|
||||
self.dataA_delegate?.moveRow(merged, from: i, to: c)
|
||||
} else {
|
||||
self.dataA.insert(parent, at: c)
|
||||
self.dataB.insert(parts, at: c)
|
||||
self.dataA_delegate?.insertRow(parent, at: c)
|
||||
}
|
||||
c += 1
|
||||
self.latestModification = max(parent.lastModified, self.latestModification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mergeExistingParts(_ dom: String, at index: Int, newChildren: [GroupedDomain]) {
|
||||
let tvc = dataB_delegate(dom)
|
||||
var i = 0
|
||||
for child in newChildren {
|
||||
if let u = dataB[index].firstIndex(where: { $0.domain == child.domain }) {
|
||||
let merged = child + dataB[index].remove(at: u)
|
||||
dataB[index].insert(merged, at: i)
|
||||
tvc?.moveRow(merged, from: u, to: i)
|
||||
} else {
|
||||
dataB[index].insert(child, at: i)
|
||||
tvc?.insertRow(child, at: i)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Delete History
|
||||
|
||||
func deleteHistory() {
|
||||
DispatchQueue.global().async {
|
||||
try? AppDB?.destroyContent()
|
||||
AppDB?.vacuum()
|
||||
self.dataAB_init()
|
||||
}
|
||||
}
|
||||
|
||||
func deleteHistory(domain: String, since ts: Timestamp) {
|
||||
DispatchQueue.global().async {
|
||||
let modified = (try? AppDB?.deleteRows(matching: domain, since: ts)) ?? 0
|
||||
guard modified > 0 else {
|
||||
return // nothing has changed
|
||||
}
|
||||
AppDB?.vacuum()
|
||||
self.Q.async(flags: .barrier) {
|
||||
guard let index = self.dataA_index(of: domain) else {
|
||||
return // nothing has changed
|
||||
}
|
||||
let parentDom = self.dataA[index].domain
|
||||
guard let list = AppDB?.domainList(matching: parentDom), list.count > 0 else {
|
||||
self.dataA.remove(at: index)
|
||||
self.dataB.remove(at: index)
|
||||
self.dataA_delegate?.deleteRow(at: index)
|
||||
self.dataB_delegate(parentDom)?.replaceData(with: [])
|
||||
return // nothing left, after deleting matching rows
|
||||
}
|
||||
// else: incremental update, replace whole list
|
||||
self.dataA[index] = list.merge(parentDom, options: self.dataF[parentDom])
|
||||
self.dataA_delegate?.replaceRow(self.dataA[index], at: index)
|
||||
self.dataB[index].removeAll()
|
||||
for var child in list {
|
||||
child.options = self.dataF[child.domain]
|
||||
self.dataB[index].append(child)
|
||||
}
|
||||
self.dataB_delegate(parentDom)?.replaceData(with: self.dataB[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Partial Update Filter
|
||||
|
||||
func updateFilter(_ domain: String, add: FilterOptions) {
|
||||
updateFilter(domain, set: (dataF[domain] ?? FilterOptions()).union(add))
|
||||
}
|
||||
|
||||
func updateFilter(_ domain: String, remove: FilterOptions) {
|
||||
updateFilter(domain, set: dataF[domain]?.subtracting(remove))
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - set: Remove a filter with `nil` or `.none`
|
||||
private func updateFilter(_ domain: String, set: FilterOptions?) {
|
||||
AppDB?.setFilter(domain, set)
|
||||
Q.async(flags: .barrier) {
|
||||
self.dataF[domain] = set
|
||||
if let i = self.dataA_index(of: domain) {
|
||||
if domain == self.dataA[i].domain {
|
||||
self.dataA[i].options = (set == FilterOptions.none) ? nil : set
|
||||
self.dataA_delegate?.replaceRow(self.dataA[i], at: i)
|
||||
}
|
||||
if let u = self.dataB[i].firstIndex(where: { $0.domain == domain }) {
|
||||
self.dataB[i][u].options = (set == FilterOptions.none) ? nil : set
|
||||
self.dataB_delegate(self.dataA[i].domain)?.replaceRow(self.dataB[i][u], at: u)
|
||||
}
|
||||
}
|
||||
NotifyFilterChanged.postOnMainThread()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper methods
|
||||
|
||||
private func dataA_index(of domain: String) -> Int? {
|
||||
dataA.firstIndex { domain.isSubdomain(of: $0.domain) }
|
||||
}
|
||||
|
||||
private func groupBySubdomains(_ allDomains: [GroupedDomain]) -> [(parent: GroupedDomain, parts: [GroupedDomain])] {
|
||||
var i: Int = 0
|
||||
var indexOf: [String: Int] = [:]
|
||||
var res: [(domain: String, list: [GroupedDomain])] = []
|
||||
for var x in allDomains {
|
||||
let domain = x.domain.splitDomainAndHost().domain
|
||||
x.options = dataF[x.domain]
|
||||
if let y = indexOf[domain] {
|
||||
res[y].list.append(x)
|
||||
} else {
|
||||
res.append((domain, [x]))
|
||||
indexOf[domain] = i
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
return res.map { ($1.merge($0, options: self.dataF[$0]), $1) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
extension DBWrapper {
|
||||
private func generateTestData() {
|
||||
guard let db = AppDB else { return }
|
||||
let deleted = (try? db.deleteRows(matching: "test.com")) ?? 0
|
||||
QLog.Debug("Deleting \(deleted) rows matching 'test.com'")
|
||||
|
||||
QLog.Debug("Writing 33 test logs")
|
||||
try? db.insertDNSQuery("keeptest.com", blocked: false)
|
||||
for _ in 1...4 { try? db.insertDNSQuery("test.com", blocked: false) }
|
||||
for _ in 1...7 { try? db.insertDNSQuery("i.test.com", blocked: false) }
|
||||
for i in 1...8 { try? db.insertDNSQuery("b.test.com", blocked: i>5) }
|
||||
for i in 1...13 { try? db.insertDNSQuery("bi.test.com", blocked: i%2==0) }
|
||||
|
||||
QLog.Debug("Creating 4 filters")
|
||||
db.setFilter("b.test.com", .blocked)
|
||||
db.setFilter("i.test.com", .ignored)
|
||||
db.setFilter("bi.test.com", [.blocked, .ignored])
|
||||
|
||||
QLog.Debug("Done")
|
||||
}
|
||||
|
||||
@objc private func insertRandomEntry() {
|
||||
QLog.Debug("Inserting 1 periodic log entry")
|
||||
try? AppDB?.insertDNSQuery("\(arc4random() % 5).count.test.com", blocked: true)
|
||||
}
|
||||
}
|
||||
302
main/DB/SQDB.swift
Normal file
302
main/DB/SQDB.swift
Normal file
@@ -0,0 +1,302 @@
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
let exportPath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
let basePath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck")
|
||||
let DB_PATH = basePath!.appendingPathComponent("dns-logs.sqlite").relativePath
|
||||
|
||||
typealias Timestamp = Int64
|
||||
struct GroupedDomain {
|
||||
let domain: String, total: Int32, blocked: Int32, lastModified: Timestamp
|
||||
var options: FilterOptions? = nil
|
||||
}
|
||||
|
||||
struct FilterOptions: OptionSet {
|
||||
let rawValue: Int32
|
||||
static let none = FilterOptions(rawValue: 0)
|
||||
static let blocked = FilterOptions(rawValue: 1 << 0)
|
||||
static let ignored = FilterOptions(rawValue: 1 << 1)
|
||||
static let any = FilterOptions(rawValue: 0b11)
|
||||
}
|
||||
|
||||
enum SQLiteError: Error {
|
||||
case OpenDatabase(message: String)
|
||||
case Prepare(message: String)
|
||||
case Step(message: String)
|
||||
case Bind(message: String)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - SQLiteDatabase
|
||||
|
||||
var AppDB: SQLiteDatabase? { get { try? SQLiteDatabase.open(path: DB_PATH) } }
|
||||
|
||||
class SQLiteDatabase {
|
||||
private let dbPointer: OpaquePointer?
|
||||
private init(dbPointer: OpaquePointer?) {
|
||||
// print("SQLite path: \(basePath!.absoluteString)")
|
||||
self.dbPointer = dbPointer
|
||||
}
|
||||
|
||||
fileprivate var errorMessage: String {
|
||||
if let errorPointer = sqlite3_errmsg(dbPointer) {
|
||||
let errorMessage = String(cString: errorPointer)
|
||||
return errorMessage
|
||||
} else {
|
||||
return "No error message provided from sqlite."
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
sqlite3_close(dbPointer)
|
||||
// SQLiteDatabase.destroyDatabase(path: DB_PATH)
|
||||
}
|
||||
|
||||
static func destroyDatabase(path: String) {
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
do { try FileManager.default.removeItem(atPath: path) }
|
||||
catch { print("Could not destroy database file: \(path)") }
|
||||
}
|
||||
}
|
||||
|
||||
// static func export() throws -> URL {
|
||||
// let fmt = DateFormatter()
|
||||
// fmt.dateFormat = "yyyy-MM-dd"
|
||||
// let dest = exportPath.appendingPathComponent("\(fmt.string(from: Date()))-dns-log.sqlite")
|
||||
// try? FileManager.default.removeItem(at: dest)
|
||||
// try FileManager.default.copyItem(atPath: DB_PATH, toPath: dest.relativePath)
|
||||
// return dest
|
||||
// }
|
||||
|
||||
static func open(path: String) throws -> SQLiteDatabase {
|
||||
var db: OpaquePointer?
|
||||
//sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_SHAREDCACHE, nil)
|
||||
if sqlite3_open(path, &db) == SQLITE_OK {
|
||||
return SQLiteDatabase(dbPointer: db)
|
||||
} else {
|
||||
defer {
|
||||
if db != nil {
|
||||
sqlite3_close(db)
|
||||
}
|
||||
}
|
||||
if let errorPointer = sqlite3_errmsg(db) {
|
||||
let message = String(cString: errorPointer)
|
||||
throw SQLiteError.OpenDatabase(message: message)
|
||||
} else {
|
||||
throw SQLiteError.OpenDatabase(message: "No error message provided from sqlite.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run<T>(sql: String, bind: ((OpaquePointer) -> Bool)?, step: (OpaquePointer) throws -> T) throws -> T {
|
||||
var statement: OpaquePointer?
|
||||
guard sqlite3_prepare_v2(dbPointer, sql, -1, &statement, nil) == SQLITE_OK,
|
||||
let stmt = statement else {
|
||||
throw SQLiteError.Prepare(message: errorMessage)
|
||||
}
|
||||
defer { sqlite3_finalize(stmt) }
|
||||
guard bind?(stmt) ?? true else {
|
||||
throw SQLiteError.Bind(message: errorMessage)
|
||||
}
|
||||
return try step(stmt)
|
||||
}
|
||||
|
||||
func ifStep(_ stmt: OpaquePointer, _ expected: Int32) throws {
|
||||
guard sqlite3_step(stmt) == expected else {
|
||||
throw SQLiteError.Step(message: errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func createTable(table: SQLTable.Type) throws {
|
||||
try run(sql: table.createStatement, bind: nil) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
func vacuum() {
|
||||
try? run(sql: "VACUUM;", bind: nil) { try ifStep($0, SQLITE_DONE) }
|
||||
}
|
||||
}
|
||||
|
||||
protocol SQLTable {
|
||||
static var createStatement: String { get }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Easy Access func
|
||||
|
||||
private extension SQLiteDatabase {
|
||||
func bindInt(_ stmt: OpaquePointer, _ col: Int32, _ value: Int32) -> Bool {
|
||||
sqlite3_bind_int(stmt, col, value) == SQLITE_OK
|
||||
}
|
||||
|
||||
func bindInt64(_ stmt: OpaquePointer, _ col: Int32, _ value: sqlite3_int64) -> Bool {
|
||||
sqlite3_bind_int64(stmt, col, value) == SQLITE_OK
|
||||
}
|
||||
|
||||
func bindText(_ stmt: OpaquePointer, _ col: Int32, _ value: String) -> Bool {
|
||||
sqlite3_bind_text(stmt, col, (value as NSString).utf8String, -1, nil) == SQLITE_OK
|
||||
}
|
||||
|
||||
func readText(_ stmt: OpaquePointer, _ col: Int32) -> String? {
|
||||
let val = sqlite3_column_text(stmt, col)
|
||||
return (val != nil ? String(cString: val!) : nil)
|
||||
}
|
||||
|
||||
func readGroupedDomain(_ stmt: OpaquePointer) -> GroupedDomain {
|
||||
GroupedDomain(domain: readText(stmt, 0) ?? "",
|
||||
total: sqlite3_column_int(stmt, 1),
|
||||
blocked: sqlite3_column_int(stmt, 2),
|
||||
lastModified: sqlite3_column_int64(stmt, 3))
|
||||
}
|
||||
|
||||
func allRows<T>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> T) -> [T] {
|
||||
var r: [T] = []
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) { r.append(fn(stmt)) }
|
||||
return r
|
||||
}
|
||||
|
||||
func allRowsKeyed<T,U>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> (key: T, value: U)) -> [T:U] {
|
||||
var r: [T:U] = [:]
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) { let (k,v) = fn(stmt); r[k] = v }
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DNSQuery
|
||||
|
||||
struct DNSQuery: SQLTable {
|
||||
let ts: Timestamp
|
||||
let domain: String
|
||||
let wasBlocked: Bool
|
||||
let options: FilterOptions
|
||||
static var createStatement: String {
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS req(
|
||||
ts BIGINT DEFAULT (strftime('%s','now')),
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
logOpt INT DEFAULT 0
|
||||
);
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: insert
|
||||
|
||||
func insertDNSQuery(_ domain: String, blocked: Bool) throws {
|
||||
try? run(sql: "INSERT INTO req (domain, logOpt) VALUES (?, ?);", bind: {
|
||||
self.bindText($0, 1, domain) && self.bindInt($0, 2, blocked ? 1 : 0)
|
||||
}) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: delete
|
||||
|
||||
func destroyContent() throws {
|
||||
try? run(sql: "DROP TABLE IF EXISTS req;", bind: nil) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
try? createTable(table: DNSQuery.self)
|
||||
}
|
||||
|
||||
/// Delete rows matching `ts >= ? AND "domain" OR "*.domain"`
|
||||
@discardableResult func deleteRows(matching domain: String, since ts: Timestamp = 0) throws -> Int32 {
|
||||
try run(sql: "DELETE FROM req WHERE ts >= ? AND (domain = ? OR domain LIKE '%.' || ?);", bind: {
|
||||
self.bindInt64($0, 1, ts) && self.bindText($0, 2, domain) && self.bindText($0, 3, domain)
|
||||
}) { stmt -> Int32 in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
return sqlite3_changes(dbPointer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
func domainList(since ts: Timestamp = 0) -> [GroupedDomain]? {
|
||||
try? run(sql: "SELECT domain, COUNT(*), SUM(logOpt&1), MAX(ts) FROM req \(ts == 0 ? "" : "WHERE ts > ?") GROUP BY domain ORDER BY 4 DESC;", bind: {
|
||||
ts == 0 || self.bindInt64($0, 1, ts)
|
||||
}) {
|
||||
allRows($0) { readGroupedDomain($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func domainList(matching domain: String) -> [GroupedDomain]? {
|
||||
try? run(sql: "SELECT domain, COUNT(*), SUM(logOpt&1), MAX(ts) FROM req WHERE (domain = ? OR domain LIKE '%.' || ?) GROUP BY domain ORDER BY 4 DESC;", bind: {
|
||||
self.bindText($0, 1, domain) && self.bindText($0, 2, domain)
|
||||
}) {
|
||||
allRows($0) { readGroupedDomain($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func timesForDomain(_ fullDomain: String) -> [(Timestamp, Bool)]? {
|
||||
try? run(sql: "SELECT ts, logOpt FROM req WHERE domain = ?;", bind: {
|
||||
self.bindText($0, 1, fullDomain)
|
||||
}) {
|
||||
allRows($0) { (sqlite3_column_int64($0, 0), sqlite3_column_int($0, 1) > 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DNSFilter
|
||||
|
||||
struct DNSFilter: SQLTable {
|
||||
let domain: String
|
||||
let options: FilterOptions
|
||||
static var createStatement: String {
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS filter(
|
||||
domain VARCHAR(255) UNIQUE NOT NULL,
|
||||
opt INT DEFAULT 0
|
||||
);
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: read
|
||||
|
||||
func loadFilters() -> [String : FilterOptions]? {
|
||||
try? run(sql: "SELECT domain, opt FROM filter ORDER BY domain ASC;", bind: nil) {
|
||||
allRowsKeyed($0) {
|
||||
(key: readText($0, 0) ?? "",
|
||||
value: FilterOptions(rawValue: sqlite3_column_int($0, 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: write
|
||||
|
||||
func setFilter(_ domain: String, _ value: FilterOptions?) {
|
||||
func removeFilter() {
|
||||
try? run(sql: "DELETE FROM filter WHERE domain = ? LIMIT 1;", bind: {
|
||||
self.bindText($0, 1, domain)
|
||||
}) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
guard let rv = value?.rawValue, rv > 0 else {
|
||||
removeFilter()
|
||||
return
|
||||
}
|
||||
func createFilter() throws {
|
||||
try run(sql: "INSERT OR FAIL INTO filter (domain, opt) VALUES (?, ?);", bind: {
|
||||
self.bindText($0, 1, domain) && self.bindInt($0, 2, rv)
|
||||
}) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
func updateFilter() {
|
||||
try? run(sql: "UPDATE filter SET opt = ? WHERE domain = ? LIMIT 1;", bind: {
|
||||
self.bindInt($0, 1, rv) && self.bindText($0, 2, domain)
|
||||
}) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
do { try createFilter() } catch { updateFilter() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user