diff --git a/Mac/Browser.swift b/Mac/Browser.swift index d574bc7ad..e2558667a 100644 --- a/Mac/Browser.swift +++ b/Mac/Browser.swift @@ -43,7 +43,7 @@ struct Browser { /// - Note: Some browsers (specifically Chromium-derived ones) will ignore the request /// to open in the background. static func open(_ urlString: String, inBackground: Bool) { - guard let url = URL(unicodeString: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return } + guard let url = URL(string: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return } let configuration = NSWorkspace.OpenConfiguration() configuration.requiresUniversalLinks = true diff --git a/Mac/Inspector/WebFeedInspectorViewController.swift b/Mac/Inspector/WebFeedInspectorViewController.swift index 39d82a9dc..27ef8ab2c 100644 --- a/Mac/Inspector/WebFeedInspectorViewController.swift +++ b/Mac/Inspector/WebFeedInspectorViewController.swift @@ -162,11 +162,11 @@ private extension WebFeedInspectorViewController { } func updateHomePageURL() { - homePageURLTextField?.stringValue = feed?.homePageURL?.decodedURLString ?? "" + homePageURLTextField?.stringValue = feed?.homePageURL ?? "" } func updateFeedURL() { - urlTextField?.stringValue = feed?.url.decodedURLString ?? "" + urlTextField?.stringValue = feed?.url ?? "" } func updateNotifyAboutNewArticles() { diff --git a/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift b/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift index 16c8a4d72..207a05a60 100644 --- a/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift +++ b/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift @@ -91,7 +91,7 @@ class AddWebFeedWindowController : NSWindowController, AddFeedWindowController { cancelSheet() return; } - guard let url = URL(unicodeString: normalizedURLString) else { + guard let url = URL(string: normalizedURLString) else { cancelSheet() return } diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index 3d9d4256b..3944c1bfd 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -216,16 +216,16 @@ private extension SidebarViewController { } if let homePageURL = webFeed.homePageURL, let _ = URL(string: homePageURL) { - let item = menuItem(NSLocalizedString("Open Home Page", comment: "Command"), #selector(openHomePageFromContextualMenu(_:)), homePageURL.decodedURLString ?? homePageURL) + let item = menuItem(NSLocalizedString("Open Home Page", comment: "Command"), #selector(openHomePageFromContextualMenu(_:)), homePageURL) menu.addItem(item) menu.addItem(NSMenuItem.separator()) } - let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), webFeed.url.decodedURLString ?? webFeed.url) + let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), webFeed.url) menu.addItem(copyFeedURLItem) if let homePageURL = webFeed.homePageURL { - let item = menuItem(NSLocalizedString("Copy Home Page URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), homePageURL.decodedURLString ?? homePageURL) + let item = menuItem(NSLocalizedString("Copy Home Page URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), homePageURL) menu.addItem(item) } menu.addItem(NSMenuItem.separator()) diff --git a/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift b/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift index 42a942b37..220192583 100644 --- a/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift +++ b/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift @@ -216,7 +216,7 @@ private extension LocalAccountRefresher { if let url = urlCache[urlString] { return url } - if let url = URL(unicodeString: urlString) { + if let url = URL(string: urlString) { urlCache[urlString] = url return url } diff --git a/Modules/RSCore/Sources/RSCore/Shared/ManagedResourceFile.swift b/Modules/RSCore/Sources/RSCore/Shared/ManagedResourceFile.swift deleted file mode 100644 index ea1652abe..000000000 --- a/Modules/RSCore/Sources/RSCore/Shared/ManagedResourceFile.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// ManagedResourceFile.swift -// RSCore -// -// Created by Maurice Parker on 9/13/19. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public final class ManagedResourceFile: NSObject, NSFilePresenter { - - private var isDirty = false { - didSet { - queueSaveToDiskIfNeeded() - } - } - - private var isLoading = false - private let fileURL: URL - private let operationQueue: OperationQueue - private var saveQueue: CoalescingQueue - - private let loadCallback: () -> Void - private let saveCallback: () -> Void - - public var saveInterval: TimeInterval = 5.0 { - didSet { - saveQueue.performCallsImmediately() - saveQueue = CoalescingQueue(name: "ManagedResourceFile Save Queue", interval: saveInterval) - } - } - - public var presentedItemURL: URL? { - return fileURL - } - - public var presentedItemOperationQueue: OperationQueue { - return operationQueue - } - - public init(fileURL: URL, load: @escaping () -> Void, save: @escaping () -> Void) { - - self.fileURL = fileURL - self.loadCallback = load - self.saveCallback = save - - saveQueue = CoalescingQueue(name: "ManagedResourceFile Save Queue", interval: saveInterval) - operationQueue = OperationQueue() - operationQueue.qualityOfService = .userInteractive - operationQueue.maxConcurrentOperationCount = 1 - - super.init() - - NSFileCoordinator.addFilePresenter(self) - } - - public func presentedItemDidChange() { - guard !isDirty else { return } - DispatchQueue.main.async { - self.load() - } - } - - public func savePresentedItemChanges(completionHandler: @escaping (Error?) -> Void) { - saveIfNecessary() - completionHandler(nil) - } - - public func relinquishPresentedItem(toReader reader: @escaping ((() -> Void)?) -> Void) { - saveQueue.isPaused = true - reader() { - self.saveQueue.isPaused = false - } - } - - public func relinquishPresentedItem(toWriter writer: @escaping ((() -> Void)?) -> Void) { - saveQueue.isPaused = true - writer() { - self.saveQueue.isPaused = false - } - } - - public func markAsDirty() { - if !isLoading { - isDirty = true - } - } - - public func queueSaveToDiskIfNeeded() { - saveQueue.add(self, #selector(saveToDiskIfNeeded)) - } - - public func load() { - isLoading = true - loadCallback() - isLoading = false - } - - public func saveIfNecessary() { - saveQueue.performCallsImmediately() - } - - public func resume() { - NSFileCoordinator.addFilePresenter(self) - } - - public func suspend() { - NSFileCoordinator.removeFilePresenter(self) - } - - deinit { - NSFileCoordinator.removeFilePresenter(self) - } - -} - -private extension ManagedResourceFile { - - @objc func saveToDiskIfNeeded() { - if isDirty { - isDirty = false - saveCallback() - } - } - -} diff --git a/Modules/RSDatabase/Package.swift b/Modules/RSDatabase/Package.swift index d3e24fba8..d52903088 100644 --- a/Modules/RSDatabase/Package.swift +++ b/Modules/RSDatabase/Package.swift @@ -19,8 +19,8 @@ let package = Package( targets: [ .target( name: "RSDatabase", - dependencies: ["RSDatabaseObjC"], - exclude: ["ODB/README.markdown"]), + dependencies: ["RSDatabaseObjC"] + ), .target( name: "RSDatabaseObjC", dependencies: [] diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODB.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODB.swift deleted file mode 100644 index 10783ef98..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODB.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// ODB.swift -// RSDatabase -// -// Created by Brent Simmons on 4/20/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSDatabaseObjC - -// This is not thread-safe. Neither are the other ODB* objects and structs. -// It’s up to the caller to implement thread safety. - -public final class ODB: Hashable { - - public let filepath: String - - public var isClosed: Bool { - return _closed - } - - static let rootTableID = -1 - public lazy var rootTable: ODBTable? = { - ODBTable(uniqueID: ODB.rootTableID, name: ODBPath.rootTableName, parentTable: nil, isRootTable: true, odb: self) - }() - - private var _closed = false - private let queue: RSDatabaseQueue - private var odbTablesTable: ODBTablesTable? = ODBTablesTable() - private var odbValuesTable: ODBValuesTable? = ODBValuesTable() - - public init(filepath: String) { - self.filepath = filepath - let queue = RSDatabaseQueue(filepath: filepath, excludeFromBackup: false) - queue.createTables(usingStatementsSync: ODB.tableCreationStatements) - self.queue = queue - } - - /// Call when finished, to make sure no stray references can do undefined things. - /// It’s not necessary to call this on app termination. - public func close() { - guard !_closed else { - return - } - _closed = true - queue.close() - odbValuesTable = nil - odbTablesTable = nil - rootTable?.close() - rootTable = nil - } - - /// Get a reference to an ODBTable at a path, making sure it exists. - /// Returns nil if there’s a value in the path preventing the table from being made. - public func ensureTable(_ path: ODBPath) -> ODBTable? { - return path.ensureTable(with: self) - } - - /// Compact the database on disk. - public func vacuum() { - queue.vacuum() - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(filepath) - } - - // MARK: - Equatable - - public static func ==(lhs: ODB, rhs: ODB) -> Bool { - return lhs.filepath == rhs.filepath - } -} - -extension ODB { - - func delete(_ object: ODBObject) -> Bool { - guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else { - return false - } - - if let valueObject = object as? ODBValueObject { - let uniqueID = valueObject.uniqueID - queue.updateSync { (database) in - odbValuesTable.deleteObject(uniqueID: uniqueID, database: database) - } - } - else if let tableObject = object as? ODBTable { - let uniqueID = tableObject.uniqueID - queue.updateSync { (database) in - odbTablesTable.deleteTable(uniqueID: uniqueID, database: database) - } - } - return true - } - - func deleteChildren(of table: ODBTable) -> Bool { - guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else { - return false - } - - let parentUniqueID = table.uniqueID - queue.updateSync { (database) in - odbTablesTable.deleteChildTables(parentUniqueID: parentUniqueID, database: database) - odbValuesTable.deleteChildObjects(parentUniqueID: parentUniqueID, database: database) - } - return true - } - - func insertTable(name: String, parent: ODBTable) -> ODBTable? { - guard let odbTablesTable = odbTablesTable else { - return nil - } - - var table: ODBTable? = nil - queue.fetchSync { (database) in - table = odbTablesTable.insertTable(name: name, parentTable: parent, odb: self, database: database) - } - return table! - } - - func insertValueObject(name: String, value: ODBValue, parent: ODBTable) -> ODBValueObject? { - guard let odbValuesTable = odbValuesTable else { - return nil - } - - var valueObject: ODBValueObject? = nil - queue.updateSync { (database) in - valueObject = odbValuesTable.insertValueObject(name: name, value: value, parentTable: parent, database: database) - } - return valueObject! - } - - func fetchChildren(of table: ODBTable) -> ODBDictionary { - guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else { - return ODBDictionary() - } - - var children = ODBDictionary() - - queue.fetchSync { (database) in - - let tables = odbTablesTable.fetchSubtables(of: table, database: database, odb: self) - let valueObjects = odbValuesTable.fetchValueObjects(of: table, database: database) - - // Keys are lower-cased, since we case-insensitive lookups. - - for valueObject in valueObjects { - children[valueObject.name] = valueObject - } - - for table in tables { - children[table.name] = table - } - } - - return children - } -} - -private extension ODB { - - static let tableCreationStatements = """ - CREATE TABLE if not EXISTS odb_tables (id INTEGER PRIMARY KEY AUTOINCREMENT, parent_id INTEGER NOT NULL, name TEXT NOT NULL); - - CREATE TABLE if not EXISTS odb_values (id INTEGER PRIMARY KEY AUTOINCREMENT, odb_table_id INTEGER NOT NULL, name TEXT NOT NULL, primitive_type INTEGER NOT NULL, application_type TEXT, value BLOB); - - CREATE INDEX if not EXISTS odb_tables_parent_id_index on odb_tables (parent_id); - CREATE INDEX if not EXISTS odb_values_odb_table_id_index on odb_values (odb_table_id); - - CREATE TRIGGER if not EXISTS odb_tables_after_delete_trigger_delete_subtables after delete on odb_tables begin delete from odb_tables where parent_id = OLD.id; end; - CREATE TRIGGER if not EXISTS odb_tables_after_delete_trigger_delete_child_values after delete on odb_tables begin delete from odb_values where odb_table_id = OLD.id; end; - """ -} - - diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift deleted file mode 100644 index 7caacdb3b..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ODBObject.swift -// RSDatabase -// -// Created by Brent Simmons on 4/24/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public typealias ODBDictionary = [String: ODBObject] - -// ODBTable and ODBValueObject conform to ODBObject. - -public protocol ODBObject { - var name: String { get } - var parentTable: ODBTable? { get } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift deleted file mode 100644 index 4fa0e10f6..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ODBPath.swift -// RSDatabase -// -// Created by Brent Simmons on 4/21/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -/** - An ODBPath is an array like ["system", "verbs", "apps", "Xcode"]. - The first element in the array may be "root". If so, it’s ignored: "root" is implied. - An empty array or ["root"] refers to the root table. - A path does not necessarily point to something that exists. It’s like file paths or URLs. -*/ - -public struct ODBPath: Hashable { - - /// The last element in the path. May not have same capitalization as canonical name in the database. - public let name: String - - /// True if this path points to a root table. - public let isRoot: Bool - - /// Root table name. Constant. - public static let rootTableName = "root" - - /// Elements of the path minus any unneccessary initial "root" element. - public let elements: [String] - - /// ODBPath that represents the root table. - public static let root = ODBPath.path([String]()) - - /// The optional path to the parent table. Nil only if path is to the root table. - public var parentTablePath: ODBPath? { - if isRoot { - return nil - } - return ODBPath.path(Array(elements.dropLast())) - } - - private static var pathCache = [[String]: ODBPath]() - private static let pathCacheLock = NSLock() - - private init(elements: [String]) { - - let canonicalElements = ODBPath.dropLeadingRootElement(from: elements) - self.elements = canonicalElements - - if canonicalElements.count < 1 { - self.name = ODBPath.rootTableName - self.isRoot = true - } - else { - self.name = canonicalElements.last! - self.isRoot = false - } - } - - // MARK: - API - - /// Create a path. - public static func path(_ elements: [String]) -> ODBPath { - - pathCacheLock.lock() - defer { - pathCacheLock.unlock() - } - - if let cachedPath = pathCache[elements] { - return cachedPath - } - let path = ODBPath(elements: elements) - pathCache[elements] = path - return path - } - - /// Create a path by adding an element. - public func pathByAdding(_ element: String) -> ODBPath { - return ODBPath.path(elements + [element]) - } - - /// Create a path by adding an element. - public static func +(lhs: ODBPath, rhs: String) -> ODBPath { - return lhs.pathByAdding(rhs) - } - - /// Fetch the database object at this path. - public func odbObject(with odb: ODB) -> ODBObject? { - return resolvedObject(odb) - } - - /// Fetch the value at this path. - public func odbValue(with odb: ODB) -> ODBValue? { - return parentTable(with: odb)?.odbValue(name) - } - - /// Set a value for this path. Will overwrite existing value or table. - public func setODBValue(_ value: ODBValue, odb: ODB) -> Bool { - return parentTable(with: odb)?.set(value, name: name) ?? false - } - - /// Fetch the raw value at this path. - public func rawValue(with odb: ODB) -> Any? { - return parentTable(with: odb)?.rawValue(name) - } - - /// Set the raw value for this path. Will overwrite existing value or table. - @discardableResult - public func setRawValue(_ rawValue: Any, odb: ODB) -> Bool { - return parentTable(with: odb)?.set(rawValue, name: name) ?? false - } - - /// Delete value or table at this path. - public func delete(from odb: ODB) -> Bool { - return parentTable(with: odb)?.delete(name: name) ?? false - } - - /// Fetch the table at this path. - public func table(with odb: ODB) -> ODBTable? { - return odbObject(with: odb) as? ODBTable - } - - /// Fetch the parent table. Nil if this is the root table. - public func parentTable(with odb: ODB) -> ODBTable? { - return parentTablePath?.table(with: odb) - } - - /// Creates a table — will delete existing table. - public func createTable(with odb: ODB) -> ODBTable? { - return parentTable(with: odb)?.addSubtable(name: name) - } - - /// Return the table for the final item in the path. - /// Won’t delete anything. - @discardableResult - public func ensureTable(with odb: ODB) -> ODBTable? { - - if isRoot { - return odb.rootTable - } - - if let existingObject = odbObject(with: odb) { - if let existingTable = existingObject as? ODBTable { - return existingTable - } - return nil // It must be a value: don’t overwrite. - } - - if let parentTable = parentTablePath!.ensureTable(with: odb) { - return parentTable.addSubtable(name: name) - } - return nil - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(elements) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBPath, rhs: ODBPath) -> Bool { - return lhs.elements == rhs.elements - } -} - -// MARK: - Private - -private extension ODBPath { - - func resolvedObject(_ odb: ODB) -> ODBObject? { - if isRoot { - return odb.rootTable - } - guard let table = parentTable(with: odb) else { - return nil - } - return table[name] - } - - static func dropLeadingRootElement(from elements: [String]) -> [String] { - if elements.count < 1 { - return elements - } - - let firstElement = elements.first! - if firstElement == ODBPath.rootTableName { - return Array(elements.dropFirst()) - } - - return elements - } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift deleted file mode 100644 index 90d8455c0..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ODBRawValueTable.swift -// RSDatabase -// -// Created by Brent Simmons on 9/13/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -// Use this when you’re just getting/setting raw values from a table. - -public final class ODBRawValueTable { - - let table: ODBTable - - init(table: ODBTable) { - self.table = table - } - - public subscript(_ name: String) -> Any? { - get { - return table.rawValue(name) - } - set { - if let rawValue = newValue { - table.set(rawValue, name: name) - } - else { - table.delete(name: name) - } - } - } - - public func string(for name: String) -> String? { - return self[name] as? String - } - - public func setString(_ stringValue: String?, for name: String) { - self[name] = stringValue - } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift deleted file mode 100644 index 9ef4b4420..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// ODBTable.swift -// RSDatabase -// -// Created by Brent Simmons on 4/21/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public final class ODBTable: ODBObject, Hashable { - - let uniqueID: Int - public let isRootTable: Bool - public let odb: ODB - public let parentTable: ODBTable? - public let name: String - public let path: ODBPath - private var _children: ODBDictionary? - - public var children: ODBDictionary { - get { - if _children == nil { - _children = odb.fetchChildren(of: self) - } - return _children! - } - set { - _children = newValue - } - } - - public lazy var rawValueTable = { - return ODBRawValueTable(table: self) - }() - - init(uniqueID: Int, name: String, parentTable: ODBTable?, isRootTable: Bool, odb: ODB) { - self.uniqueID = uniqueID - self.name = name - self.parentTable = parentTable - self.isRootTable = isRootTable - self.path = isRootTable ? ODBPath.root : parentTable!.path + name - self.odb = odb - } - - /// Get the ODBObject for the given name. - public subscript(_ name: String) -> ODBObject? { - return children[name] - } - - /// Fetch the ODBValue for the given name. - public func odbValue(_ name: String) -> ODBValue? { - return (self[name] as? ODBValueObject)?.value - } - - /// Set the ODBValue for the given name. - public func set(_ odbValue: ODBValue, name: String) -> Bool { - // Don’t bother if key/value pair already exists. - // If child with same name exists, delete it. - - let existingObject = self[name] - if let existingValue = existingObject as? ODBValueObject, existingValue.value == odbValue { - return true - } - - guard let valueObject = odb.insertValueObject(name: name, value: odbValue, parent: self) else { - return false - } - if let existingObject = existingObject { - delete(existingObject) - } - addChild(name: name, object: valueObject) - return true - } - - /// Fetch the raw value for the given name. - public func rawValue(_ name: String) -> Any? { - return (self[name] as? ODBValueObject)?.value.rawValue - } - - /// Create a value object and set it for the given name. - @discardableResult - public func set(_ rawValue: Any, name: String) -> Bool { - guard let odbValue = ODBValue(rawValue: rawValue) else { - return false - } - return set(odbValue, name: name) - } - - /// Delete all children — empty the table. - public func deleteChildren() -> Bool { - guard odb.deleteChildren(of: self) else { - return false - } - _children = ODBDictionary() - return true - } - - /// Delete a child object. - @discardableResult - public func delete(_ object: ODBObject) -> Bool { - return odb.delete(object) - } - - /// Delete a child with the given name. - @discardableResult - public func delete(name: String) -> Bool { - guard let child = self[name] else { - return false - } - return delete(child) - } - - /// Fetch the subtable with the given name. - public func subtable(name: String) -> ODBTable? { - return self[name] as? ODBTable - } - - /// Add a subtable with the given name. Overwrites previous child with that name. - public func addSubtable(name: String) -> ODBTable? { - let existingObject = self[name] - guard let subTable = odb.insertTable(name: name, parent: self) else { - return nil - } - if let existingObject = existingObject { - delete(existingObject) - } - addChild(name: name, object: subTable) - return subTable - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(uniqueID) - hasher.combine(odb) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBTable, rhs: ODBTable) -> Bool { - return lhs.uniqueID == rhs.uniqueID && lhs.odb == rhs.odb - } -} - -extension ODBTable { - - func close() { - // Called from ODB when database is closing. - if let rawChildren = _children { - rawChildren.forEach { (key: String, value: ODBObject) in - if let table = value as? ODBTable { - table.close() - } - } - } - _children = nil - } -} - -private extension ODBTable { - - func addChild(name: String, object: ODBObject) { - children[name] = object - } - - func ensureChildren() { - let _ = children - } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift deleted file mode 100644 index 24d4764c9..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// ODBTablesTable.swift -// RSDatabase -// -// Created by Brent Simmons on 4/20/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSDatabaseObjC - -final class ODBTablesTable: DatabaseTable { - - let name = "odb_tables" - - private struct Key { - static let uniqueID = "id" - static let parentID = "parent_id" - static let name = "name" - } - - func fetchSubtables(of table: ODBTable, database: FMDatabase, odb: ODB) -> Set { - guard let rs: FMResultSet = database.executeQuery("select * from odb_tables where parent_id = ?", withArgumentsIn: [table.uniqueID]) else { - return Set() - } - return rs.mapToSet{ createTable(with: $0, parentTable: table, odb: odb) } - } - - func insertTable(name: String, parentTable: ODBTable, odb: ODB, database: FMDatabase) -> ODBTable { - let d: DatabaseDictionary = [Key.parentID: parentTable.uniqueID, Key.name: name] - insertRow(d, insertType: .normal, in: database) - let uniqueID = Int(database.lastInsertRowId()) - return ODBTable(uniqueID: uniqueID, name: name, parentTable: parentTable, isRootTable: false, odb: odb) - } - - func deleteTable(uniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.uniqueID, equalsValue: uniqueID, tableName: name) - } - - func deleteChildTables(parentUniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.parentID, equalsValue: parentUniqueID, tableName: name) - } -} - -private extension ODBTablesTable { - - func createTable(with row: FMResultSet, parentTable: ODBTable, odb: ODB) -> ODBTable? { - - guard let name = row.string(forColumn: Key.name) else { - return nil - } - let uniqueID = Int(row.longLongInt(forColumn: Key.uniqueID)) - - return ODBTable(uniqueID: uniqueID, name: name, parentTable: parentTable, isRootTable: false, odb: odb) - } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift deleted file mode 100644 index 3e6fd253b..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// ODBValue.swift -// RSDatabase -// -// Created by Brent Simmons on 4/24/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public struct ODBValue: Hashable { - - // Values are arbitrary but must not change: they’re stored in the database. - public enum PrimitiveType: Int { - case boolean=8 - case integer=16 - case double=32 - case date=64 - case string=128 - case data=256 - } - - public let rawValue: Any - public let primitiveType: PrimitiveType - public let applicationType: String? // Application-defined - - public init(rawValue: Any, primitiveType: PrimitiveType, applicationType: String?) { - self.rawValue = rawValue - self.primitiveType = primitiveType - self.applicationType = applicationType - } - - public init(rawValue: Any, primitiveType: PrimitiveType) { - self.init(rawValue: rawValue, primitiveType: primitiveType, applicationType: nil) - } - - public init?(rawValue: Any) { - guard let primitiveType = ODBValue.primitiveTypeForRawValue(rawValue) else { - return nil - } - self.init(rawValue: rawValue, primitiveType: primitiveType) - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - if let booleanValue = rawValue as? Bool { - hasher.combine(booleanValue) - } - else if let integerValue = rawValue as? Int { - hasher.combine(integerValue) - } - else if let doubleValue = rawValue as? Double { - hasher.combine(doubleValue) - } - else if let stringValue = rawValue as? String { - hasher.combine(stringValue) - } - else if let dataValue = rawValue as? Data { - hasher.combine(dataValue) - } - else if let dateValue = rawValue as? Date { - hasher.combine(dateValue) - } - - hasher.combine(primitiveType) - hasher.combine(applicationType) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBValue, rhs: ODBValue) -> Bool { - - if lhs.primitiveType != rhs.primitiveType || lhs.applicationType != rhs.applicationType { - return false - } - - switch lhs.primitiveType { - case .boolean: - return compareBooleans(lhs.rawValue, rhs.rawValue) - case .integer: - return compareIntegers(lhs.rawValue, rhs.rawValue) - case .double: - return compareDoubles(lhs.rawValue, rhs.rawValue) - case .string: - return compareStrings(lhs.rawValue, rhs.rawValue) - case .data: - return compareData(lhs.rawValue, rhs.rawValue) - case .date: - return compareDates(lhs.rawValue, rhs.rawValue) - } - } -} - -private extension ODBValue { - - static func compareBooleans(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Bool, let right = right as? Bool else { - return false - } - return left == right - } - - static func compareIntegers(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Int, let right = right as? Int else { - return false - } - return left == right - } - - static func compareDoubles(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Double, let right = right as? Double else { - return false - } - return left == right - } - - static func compareStrings(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? String, let right = right as? String else { - return false - } - return left == right - } - - static func compareData(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Data, let right = right as? Data else { - return false - } - return left == right - } - - static func compareDates(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Date, let right = right as? Date else { - return false - } - return left == right - } - - static func primitiveTypeForRawValue(_ rawValue: Any) -> ODBValue.PrimitiveType? { - - switch rawValue { - case is Bool: - return .boolean - case is Int: - return .integer - case is Double: - return .double - case is Date: - return .date - case is String: - return .string - case is Data: - return .data - default: - return nil - } - } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift deleted file mode 100644 index a622197ed..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ODBValueObject.swift -// RSDatabase -// -// Created by Brent Simmons on 4/21/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public struct ODBValueObject: ODBObject, Hashable { - - let uniqueID: Int - public let value: ODBValue - - // ODBObject protocol properties - public let name: String - public let parentTable: ODBTable? - - init(uniqueID: Int, parentTable: ODBTable, name: String, value: ODBValue) { - - self.uniqueID = uniqueID - self.parentTable = parentTable - self.name = name - self.value = value - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(uniqueID) - hasher.combine(value) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBValueObject, rhs: ODBValueObject) -> Bool { - return lhs.uniqueID == rhs.uniqueID && lhs.value == rhs.value - } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift b/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift deleted file mode 100644 index c86356a3f..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// ODBValuesTable.swift -// RSDatabase -// -// Created by Brent Simmons on 4/20/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSDatabaseObjC - -final class ODBValuesTable: DatabaseTable { - - let name = "odb_values" - - private struct Key { - static let uniqueID = "id" - static let parentID = "odb_table_id" - static let name = "name" - static let primitiveType = "primitive_type" - static let applicationType = "application_type" - static let value = "value" - } - - func fetchValueObjects(of table: ODBTable, database: FMDatabase) -> Set { - guard let rs = database.rs_selectRowsWhereKey(Key.parentID, equalsValue: table.uniqueID, tableName: name) else { - return Set() - } - return rs.mapToSet{ valueObject(with: $0, parentTable: table) } - } - - func deleteObject(uniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.uniqueID, equalsValue: uniqueID, tableName: name) - } - - func deleteChildObjects(parentUniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.parentID, equalsValue: parentUniqueID, tableName: name) - } - - func insertValueObject(name: String, value: ODBValue, parentTable: ODBTable, database: FMDatabase) -> ODBValueObject { - - var d: DatabaseDictionary = [Key.parentID: parentTable.uniqueID, Key.name: name, Key.primitiveType: value.primitiveType.rawValue, Key.value: value.rawValue] - if let applicationType = value.applicationType { - d[Key.applicationType] = applicationType - } - - insertRow(d, insertType: .normal, in: database) - let uniqueID = Int(database.lastInsertRowId()) - return ODBValueObject(uniqueID: uniqueID, parentTable: parentTable, name: name, value: value) - } -} - -private extension ODBValuesTable { - - func valueObject(with row: FMResultSet, parentTable: ODBTable) -> ODBValueObject? { - - guard let value = value(with: row) else { - return nil - } - guard let name = row.string(forColumn: Key.name) else { - return nil - } - let uniqueID = Int(row.longLongInt(forColumn: Key.uniqueID)) - - return ODBValueObject(uniqueID: uniqueID, parentTable: parentTable, name: name, value: value) - } - - func value(with row: FMResultSet) -> ODBValue? { - - guard let primitiveType = ODBValue.PrimitiveType(rawValue: Int(row.longLongInt(forColumn: Key.primitiveType))) else { - return nil - } - var value: Any? = nil - - switch primitiveType { - case .boolean: - value = row.bool(forColumn: Key.value) - case .integer: - value = Int(row.longLongInt(forColumn: Key.value)) - case .double: - value = row.double(forColumn: Key.value) - case .string: - value = row.string(forColumn: Key.value) - case .data: - value = row.data(forColumn: Key.value) - case .date: - value = row.date(forColumn: Key.value) - } - - guard let fetchedValue = value else { - return nil - } - - let applicationType = row.string(forColumn: Key.applicationType) - return ODBValue(rawValue: fetchedValue, primitiveType: primitiveType, applicationType: applicationType) - } -} diff --git a/Modules/RSDatabase/Sources/RSDatabase/ODB/README.markdown b/Modules/RSDatabase/Sources/RSDatabase/ODB/README.markdown deleted file mode 100644 index 4c7e8b4d6..000000000 --- a/Modules/RSDatabase/Sources/RSDatabase/ODB/README.markdown +++ /dev/null @@ -1,149 +0,0 @@ -# ODB - -**NOTE**: This all has been excluded from building. It’s a work in progress, not ready for use. - -ODB stands for Object Database. - -“Object” doesn’t mean object in the object-oriented programming sense — it just means *thing*. - -Think of the ODB as a nested Dictionary that’s *persistent*. It’s schema-less. Tables (which are like dictionaries) can contain other tables. It’s all key-value pairs. - -The inspiration for this comes from [UserLand Frontier](http://frontier.userland.com/), which featured an ODB which made persistence for scripts easy. - -You could write a script like `user.personalInfo.name = "Bull Mancuso"` — and, inside the `personalInfo` table, which is inside the `user` table, it would create or set a key/value pair: `name` would be the key, and `Bull Mancuso` would be the value. - -Looking up the value later was as simple as referring to `user.personalInfo.name`. - -This ODB implementation does *not* provide that scripting language. It also does not provide a user interface for the database (Frontier did). It provides just the lowest level: the actual storage and a Swift API for getting, setting, and deleting tables and values. - -It’s built on top of SQLite. It may sound weird to build an ODB on top of a SQL database — but SQLite is amazingly robust and fast, and it’s the hammer I know best. - -My hunch is that lots of apps could benefit from this kind of storage. It was the *only* kind I used for seven years in my early career, and we wrote lots of powerful software using Frontier’s ODB. (Blogging, RSS, podcasting, web services over HTTP, OPML — all these were invented or popularized or fleshed-out using Frontier and its ODB. Not that I take personal credit: I was an employee of UserLand Software, and the vision was Dave Winer’s.) - -## How to use it - -### Create an ODB - -`let odb = ODB(filepath: somePath)` creates a new ODB for that path. If there’s an existing database on disk, it uses that one. Otherwise it creates a new one. - -### Ensuring that a table exists - -Let’s say you’re writing an RSS reader, and you want to make sure there’s a table at `RSS.feeds.[feedID]`. Given feedID and odb: - - let pathElements = ["RSS", "feeds", feedID] - let path = ODBPath(elements: pathElements, odb: odb) - ODB.perform { - let _ = path.ensureTable() - } - -The `ensureTable` function returns an `ODBTable`. It makes sure that the entire path exists. The only way `ensureTable` would return nil is if something in the path exists and it’s a value instead of a table. `ensureTable` never deletes anything. - -There is a similar `createTable` function that deletes any existing table at that path and then creates a new table. It does *not* ensure that the entire path exists, and it returns nil if the necessary ancestor tables don’t exist. - -Operations referencing `ODBTable` and `ODBValueObject` must be enclosed in an `ODB.perform` block. This is for thread safety. If you don’t use an `ODB.perform` block, it will crash deliberately with a `preconditionFailure`. - -You should *not* hold a reference to an `ODBTable`, `ODBValueObject`, or `ODBObject` outside of the `perform` block. You *can* hold a reference to an `ODBPath` and to an `ODBValue`. - -An `ODBObject` is either an `ODBTable` or `ODBValueObject`: it’s a protocol. - -### Setting a value - -Let’s say the user of your RSS reader can edit the name of a feed, and you want to store the edited name in the database. The key for the value is `editedName`. Assume that you’ve already used `ensureTable` as above. - - let path = ODBPath(elements: ["RSS", "feeds", feedID, "editedName"], odb: odb) - let value = ODBValue(value: name, primitiveType: .string, applicationType: nil) - ODB.perform { - path.setValue(value) - } - -If `editedName` exists, it gets replaced. If it doesn’t exist, then it gets created. - -(Yes, this would be so much easier in a scripting language. You’d just write: `RSS.feeds.[feedID].editedName = name` — the above is the equivalent of that.) - -See `ODBValue` for the different types of values that can be stored. Each value must be one of a few primitive types — string, date, data, etc. — but each value can optionally have its own `applicationType`. For instance, you might store OPML text as a string, but then give it an `applicationType` of `"OPML"`, so that your application knows what it is and can encode/decode properly. This lets you store values of any arbitrary complexity. - -In general, it’s good practice to use that ability sparingly. When you can break things down into simple primitive types, that’s best. Treating an entire table, with multiple stored values, as a unit is often the way to go. But not always. - -### Getting a value - -Let’s say you want to get back the edited name of the feed. You’d create the path the same way as before. And then: - - var nameValue: ODBValue? = nil - ODB.perform { - nameValue = path.value - } - let name = nameValue? as? String - -The above is written to demonstrate that you can refer to `ODBValue` outside of a `perform` call. It’s an immutable struct with no connection to the database. But in reality you’d probably write the above code more like this: - - var name: String? - ODB.perform { - name = path.value? as? String - } - -It’s totally a-okay to use Swift’s built-in types this way instead of checking the ODBValue’s `primitiveType`. The primitive types map directly to `Bool`, `Int`, `Double`, `Date`, `String`, and `Data`. - -### Deleting a table or value - -Say the user undoes editing the feed’s name, and now you want to delete `RSS.feeds.[feedID].editedName` — given the path, you’d do this: - - ODB.perform { - path.delete() - } - -This works on both tables and values. You can also call `delete()` directly on an `ODBTable`, `ODBValueObject`, or `ODBObject`. - -### ODBObject - -Some functions take or return an `ODBObject`. This is a protocol — the object is either an `ODBTable` or `ODBValueObject`. - -There is useful API to be aware of in ODBObject.swift. (But, again, an `ODBObject` reference is valid only with an `ODB.perform` call.) - -### ODBTable - -You can do some of the same things you can do with an `ODBPath`. You can also get the entire dictionary of `children`, look up any child object, delete all children, add child objects, and more. - -### ODBValueObject - -You won’t use this directly all that often. It wraps an `ODBValue`, which you’ll use way more often. The useful API for `ODBValueObject` is almost entirely in `ODBObject`. - -## Notes - -### The root table - -The one table you can’t delete is the root table — every ODB has a top-level table named `root`. You don’t usually specify `root` as the first part of a path, but you could. It’s implied. - -A path like `["RSS", "feeds"]` is precisely the same as `["root", "RSS", "feeds"]` — they’re interchangeable paths. - -### Case-sensitivity - -Frontier’s object database was case-insensitive: you could refer to the "feeds" table as the "FEeDs" table — it would be the same thing. - -While I don’t know this for sure, I assume this was because the Mac’s file system is also case-insensitive. This was considered one of the user-friendly things about Macs. - -We’re preserved this: this ODB is also case-insensitive. When comparing two keys it always uses the English locale, so that results are predictable no matter what the machine’s locale actually is. This is something to be aware of. - -### Caching and Performance - -The database is cached in memory as it is used. A table’s children are not read into memory until referenced. - -For objects already in memory, reads are fast since there’s no need to query the SQLite database. - -If this caching becomes a problem in production use — if it tends to use too much memory — we’ll make it smarter. - -### Thread safety - -Why is it okay to create and refer to `ODBPath` and `ODBValue` objects outside of an `ODB.perform` call, while it’s not okay with `ODBObject`, `ODBTable`, and `ODBValueObject`? - -Because: - -`ODBPath` represents a *query* rather than a direct reference. Each time you resolve the object it points to, it recalculates. You can create paths to things that don’t exist. The database can change while you hold an `ODBPath` reference, and that’s okay: it’s by design. Just know that you might get back something different every time you refer to `path.object`, `path.value`, and `path.table`. - -`ODBValue` is an immutable struct with no connection to the database. Once you get one, it doesn’t change, even if the database object it came from changes. (In general these will be short-lived — you just use them for wrapping and unwrapping your app’s data.) - -On the other hand, `ODBObject`, `ODBTable`, and `ODBValueObject` are direct references to the database. To prevent conflicts and maintain the structure of the database properly, it’s necessary to use a lock when working with these — that’s what `ODB.perform` does. - -Say you have a particular table that your app uses a lot. It would seem natural to want to keep a reference to that particular `ODBTable`. Instead, create and keep a reference to an `ODBPath` and refer to `path.table` inside an `ODB.perform` block when you need the table. - - - diff --git a/Modules/RSDatabase/Tests/RSDatabaseTests/DatabaseTests.swift b/Modules/RSDatabase/Tests/RSDatabaseTests/DatabaseTests.swift new file mode 100644 index 000000000..38c00b9b3 --- /dev/null +++ b/Modules/RSDatabase/Tests/RSDatabaseTests/DatabaseTests.swift @@ -0,0 +1,35 @@ +// +// DatabaseTests.swift +// RSDatabase +// +// Created by Brent Simmons on 4/24/25. +// + +import XCTest + +final class DatabaseTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Modules/RSDatabase/Tests/RSDatabaseTests/ODBTests.swift b/Modules/RSDatabase/Tests/RSDatabaseTests/ODBTests.swift deleted file mode 100644 index 219e60a1d..000000000 --- a/Modules/RSDatabase/Tests/RSDatabaseTests/ODBTests.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// ODBTests.swift -// RSDatabaseTests -// -// Created by Brent Simmons on 8/27/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import XCTest -import RSDatabase - -class ODBTests: XCTestCase { - - func testODBCreation() { - let odb = genericTestODB() - closeAndDelete(odb) - } - - func testSimpleBoolStorage() { - let odb = genericTestODB() - let path = ODBPath.path(["testBool"]) - path.setRawValue(true, odb: odb) - - XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) - closeAndDelete(odb) - } - - func testSimpleIntStorage() { - let odb = genericTestODB() - let path = ODBPath.path(["TestInt"]) - let intValue = 3487456 - path.setRawValue(intValue, odb: odb) - - XCTAssertEqual(path.rawValue(with: odb) as! Int, intValue) - closeAndDelete(odb) - } - - func testSimpleDoubleStorage() { - let odb = genericTestODB() - let path = ODBPath.path(["TestDouble"]) - let doubleValue = 3498.45745 - path.setRawValue(doubleValue, odb: odb) - - XCTAssertEqual(path.rawValue(with: odb) as! Double, doubleValue) - closeAndDelete(odb) - } - - func testReadSimpleBoolPerformance() { - let odb = genericTestODB() - let path = ODBPath.path(["TestBool"]) - path.setRawValue(true, odb: odb) - XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) - - self.measure { - let _ = path.rawValue(with: odb) - } - closeAndDelete(odb) - } - - func testSetSimpleUnchangingBoolPerformance() { - let odb = genericTestODB() - let path = ODBPath.path(["TestBool"]) - self.measure { - path.setRawValue(true, odb: odb) - } - closeAndDelete(odb) - } - - func testReadAndCloseAndReadSimpleBool() { - let f = pathForTestFile("testReadAndCloseAndReadSimpleBool.odb") - var odb = ODB(filepath: f) - let path = ODBPath.path(["testBool"]) - path.setRawValue(true, odb: odb) - - XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) - odb.close() - - odb = ODB(filepath: f) - XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) - closeAndDelete(odb) - } - - func testReplaceSimpleObject() { - let odb = genericTestODB() - let path = ODBPath.path(["TestValue"]) - let intValue = 3487456 - path.setRawValue(intValue, odb: odb) - - XCTAssertEqual(path.rawValue(with: odb) as! Int, intValue) - - let stringValue = "test string value" - path.setRawValue(stringValue, odb: odb) - XCTAssertEqual(path.rawValue(with: odb) as! String, stringValue) - - closeAndDelete(odb) - } - - func testEnsureTable() { - let odb = genericTestODB() - let path = ODBPath.path(["A", "B", "C", "D"]) - let _ = path.ensureTable(with: odb) - closeAndDelete(odb) - } - - func testEnsureTablePerformance() { - let odb = genericTestODB() - let path = ODBPath.path(["A", "B", "C", "D"]) - - self.measure { - let _ = path.ensureTable(with: odb) - } - - closeAndDelete(odb) - } - - func testStoreDateInSubtable() { - let odb = genericTestODB() - let path = ODBPath.path(["A", "B", "C", "D"]) - path.ensureTable(with: odb) - - let d = Date() - let datePath = path + "TestValue" - datePath.setRawValue(d, odb: odb) - XCTAssertEqual(datePath.rawValue(with: odb) as! Date, d) - closeAndDelete(odb) - } -} - -private extension ODBTests { - - func tempFolderPath() -> String { - return FileManager.default.temporaryDirectory.path - } - - func pathForTestFile(_ name: String) -> String { - let folder = tempFolderPath() - return (folder as NSString).appendingPathComponent(name) - } - - static var databaseFileID = 0; - - func pathForGenericTestFile() -> String { - ODBTests.databaseFileID += 1 - return pathForTestFile("Test\(ODBTests.databaseFileID).odb") - } - - func genericTestODB() -> ODB { - let f = pathForGenericTestFile() - return ODB(filepath: f) - } - - func closeAndDelete(_ odb: ODB) { - odb.close() - try! FileManager.default.removeItem(atPath: odb.filepath) - } -} diff --git a/Modules/RSWeb/Package.swift b/Modules/RSWeb/Package.swift index b06f12331..71a511d0e 100644 --- a/Modules/RSWeb/Package.swift +++ b/Modules/RSWeb/Package.swift @@ -8,7 +8,7 @@ let package = Package( .library( name: "RSWeb", type: .dynamic, - targets: ["RSWeb"]), + targets: ["RSWeb"]) ], dependencies: [ .package(path: "../RSParser"), @@ -20,15 +20,11 @@ let package = Package( dependencies: [ "RSParser", "RSCore" - ], - resources: [.copy("UTS46/uts46")], - swiftSettings: [.define("SWIFT_PACKAGE")]), + ] + //swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + ), .testTarget( name: "RSWebTests", - dependencies: [ - "RSWeb", - "RSParser", - "RSCore" - ]), + dependencies: ["RSWeb"]) ] ) diff --git a/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift b/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift index c59f0b7a9..c59489aeb 100644 --- a/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift +++ b/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift @@ -76,7 +76,7 @@ private extension HTMLMetadataDownloader { func downloadMetadata(_ url: String) { - guard let actualURL = URL(unicodeString: url) else { + guard let actualURL = URL(string: url) else { if Self.debugLoggingEnabled { Self.logger.debug("HTMLMetadataDownloader skipping download for \(url) because it couldn’t construct a URL.") } diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/Data+Extensions.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/Data+Extensions.swift deleted file mode 100644 index 140b6498b..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/Data+Extensions.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Data+Extensions.swift -// PunyCocoa Swift -// -// Created by Nate Weaver on 2020-04-12. -// - -import Foundation -import zlib - -extension Data { - - var crc32: UInt32 { - return self.withUnsafeBytes { - let buffer = $0.bindMemory(to: UInt8.self) - let initial = zlib.crc32(0, nil, 0) - return UInt32(zlib.crc32(initial, buffer.baseAddress, numericCast(buffer.count))) - } - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/Scanner+Extensions.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/Scanner+Extensions.swift deleted file mode 100644 index 0ffb0c425..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/Scanner+Extensions.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Scanner+Extensions.swift -// PunyCocoa Swift -// -// Created by Nate Weaver on 2020-04-20. -// - -import Foundation - -// Wrapper functions for < 10.15 compatibility -// TODO: Remove when support for < 10.15 is dropped. -extension Scanner { - - func shimScanUpToCharacters(from set: CharacterSet) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanUpToCharacters(from: set) - } else { - var str: NSString? - self.scanUpToCharacters(from: set, into: &str) - return str as String? - } - } - - func shimScanCharacters(from set: CharacterSet) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanCharacters(from: set) - } else { - var str: NSString? - self.scanCharacters(from: set, into: &str) - return str as String? - } - } - - func shimScanUpToString(_ substring: String) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanUpToString(substring) - } else { - var str: NSString? - self.scanUpTo(substring, into: &str) - return str as String? - } - } - - func shimScanString(_ searchString: String) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanString(searchString) - } else { - var str: NSString? - self.scanString(searchString, into: &str) - return str as String? - } - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/String+Punycode.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/String+Punycode.swift deleted file mode 100644 index a6afd15b0..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/String+Punycode.swift +++ /dev/null @@ -1,596 +0,0 @@ -// -// String+Punycode.swift -// Punycode -// -// Created by Nate Weaver on 2020-03-16. -// - -import Foundation - -public extension String { - - /// The IDNA-encoded representation of a Unicode domain. - /// - /// This will properly split domains on periods; e.g., - /// "www.bücher.ch" becomes "www.xn--bcher-kva.ch". - var idnaEncoded: String? { - guard let mapped = try? self.mapUTS46() else { return nil } - - let nonASCII = CharacterSet(charactersIn: UnicodeScalar(0)...UnicodeScalar(127)).inverted - var result = "" - - let s = Scanner(string: mapped.precomposedStringWithCanonicalMapping) - let dotAt = CharacterSet(charactersIn: ".@") - - while !s.isAtEnd { - if let input = s.shimScanUpToCharacters(from: dotAt) { - if !input.isValidLabel { return nil } - - if input.rangeOfCharacter(from: nonASCII) != nil { - result.append("xn--") - - if let encoded = input.punycodeEncoded { - result.append(encoded) - } - } else { - result.append(input) - } - } - - if let input = s.shimScanCharacters(from: dotAt) { - result.append(input) - } - } - - return result - } - - /// The Unicode representation of an IDNA-encoded domain. - /// - /// This will properly split domains on periods; e.g., - /// "www.xn--bcher-kva.ch" becomes "www.bücher.ch". - var idnaDecoded: String? { - var result = "" - let s = Scanner(string: self) - let dotAt = CharacterSet(charactersIn: ".@") - - while !s.isAtEnd { - if let input = s.shimScanUpToCharacters(from: dotAt) { - if input.lowercased().hasPrefix("xn--") { - let start = input.index(input.startIndex, offsetBy: 4) - guard let substr = input[start...].punycodeDecoded else { return nil } - guard substr.isValidLabel else { return nil } - result.append(substr) - } else { - result.append(input) - } - } - - if let input = s.shimScanCharacters(from: dotAt) { - result.append(input) - } - } - - return result - } - - /// The IDNA- and percent-encoded representation of a URL string. - var encodedURLString: String? { - let urlParts = self.urlParts - var pathAndQuery = urlParts.pathAndQuery - - var allowedCharacters = CharacterSet.urlPathAllowed - allowedCharacters.insert(charactersIn: "%?") - pathAndQuery = pathAndQuery.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? "" - - var result = "\(urlParts.scheme)\(urlParts.delim)" - - if let username = urlParts.username?.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) { - if let password = urlParts.password?.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) { - result.append("\(username):\(password)@") - } else { - result.append("\(username)@") - } - } - - guard let host = urlParts.host.idnaEncoded else { return nil } - - result.append("\(host)\(pathAndQuery)") - - if var fragment = urlParts.fragment { - var fragmentAlloweCharacters = CharacterSet.urlFragmentAllowed - fragmentAlloweCharacters.insert(charactersIn: "%") - fragment = fragment.addingPercentEncoding(withAllowedCharacters: fragmentAlloweCharacters) ?? "" - - result.append("#\(fragment)") - } - - return result - } - - /// The Unicode representation of an IDNA- and percent-encoded URL string. - var decodedURLString: String? { - let urlParts = self.urlParts - var usernamePassword = "" - - if let username = urlParts.username?.removingPercentEncoding { - if let password = urlParts.password?.removingPercentEncoding { - usernamePassword = "\(username):\(password)@" - } else { - usernamePassword = "\(username)@" - } - } - - guard let host = urlParts.host.idnaDecoded else { return nil } - - var result = "\(urlParts.scheme)\(urlParts.delim)\(usernamePassword)\(host)\(urlParts.pathAndQuery.removingPercentEncoding ?? "")" - - if let fragment = urlParts.fragment?.removingPercentEncoding { - result.append("#\(fragment)") - } - - return result - } - -} - -public extension URL { - - /// Initializes a URL with a Unicode URL string. - /// - /// If `unicodeString` can be successfully encoded, equivalent to - /// - /// ``` - /// URL(string: unicodeString.encodedURLString!) - /// ``` - /// - /// - Parameter unicodeString: The unicode URL string with which to create a URL. - init?(unicodeString: String) { - if let url = URL(string: unicodeString) { - self = url - return - } - - guard let encodedString = unicodeString.encodedURLString else { return nil } - self.init(string: encodedString) - } - - /// The IDNA- and percent-decoded representation of the URL. - /// - /// Equivalent to - /// - /// ``` - /// self.absoluteString.decodedURLString - /// ``` - var decodedURLString: String? { - return self.absoluteString.decodedURLString - } - - /// Initializes a URL from a relative Unicode string and a base URL. - /// - Parameters: - /// - unicodeString: The URL string with which to initialize the NSURL object. `unicodeString` is interpreted relative to `baseURL`. - /// - url: The base URL for the URL object - init?(unicodeString: String, relativeTo url: URL?) { - if let url = URL(string: unicodeString, relativeTo: url) { - self = url - return - } - - let parts = unicodeString.urlParts - - if !parts.host.isEmpty { - guard let encodedString = unicodeString.encodedURLString else { return nil } - self.init(string: encodedString, relativeTo: url) - } else { - var allowedCharacters = CharacterSet.urlPathAllowed - allowedCharacters.insert(charactersIn: "%?#") - guard let encoded = unicodeString.addingPercentEncoding(withAllowedCharacters: allowedCharacters) else { return nil } - self.init(string: encoded, relativeTo: url) - } - } - -} - -private extension StringProtocol { - - /// Punycode-encodes a string. - /// - /// Returns `nil` on error. - /// - Todo: Throw errors on failure instead of returning `nil`. - var punycodeEncoded: String? { - var result = "" - let scalars = self.unicodeScalars - let inputLength = scalars.count - - var n = Punycode.initialN - var delta: UInt32 = 0 - var outLen: UInt32 = 0 - var bias = Punycode.initialBias - - for scalar in scalars where scalar.isASCII { - result.unicodeScalars.append(scalar) - outLen += 1 - } - - let b: UInt32 = outLen - var h: UInt32 = outLen - - if b > 0 { - result.append(Punycode.delimiter) - } - - // Main encoding loop: - - while h < inputLength { - var m = UInt32.max - - for c in scalars { - if c.value >= n && c.value < m { - m = c.value - } - } - - if m - n > (UInt32.max - delta) / (h + 1) { - return nil // overflow - } - - delta += (m - n) * (h + 1) - n = m - - for c in scalars { - - if c.value < n { - delta += 1 - - if delta == 0 { - return nil // overflow - } - } - - if c.value == n { - var q = delta - var k = Punycode.base - - while true { - let t = k <= bias ? Punycode.tmin : - k >= bias + Punycode.tmax ? Punycode.tmax : k - bias - - if q < t { - break - } - - let encodedDigit = Punycode.encodeDigit(t + (q - t) % (Punycode.base - t), flag: false) - - result.unicodeScalars.append(UnicodeScalar(encodedDigit)!) - q = (q - t) / (Punycode.base - t) - - k += Punycode.base - } - - result.unicodeScalars.append(UnicodeScalar(Punycode.encodeDigit(q, flag: false))!) - bias = Punycode.adapt(delta: delta, numPoints: h + 1, firstTime: h == b) - delta = 0 - h += 1 - } - } - - delta += 1 - n += 1 - } - - return result - } - - /// Punycode-decodes a string. - /// - /// Returns `nil` on error. - /// - Todo: Throw errors on failure instead of returning `nil`. - var punycodeDecoded: String? { - var result = "" - let scalars = self.unicodeScalars - - let endIndex = scalars.endIndex - var n = Punycode.initialN - var outLen: UInt32 = 0 - var i: UInt32 = 0 - var bias = Punycode.initialBias - - var b = scalars.startIndex - - for j in scalars.indices { - if Character(self.unicodeScalars[j]) == Punycode.delimiter { - b = j - break - } - } - - for j in scalars.indices { - if j >= b { - break - } - - let scalar = scalars[j] - - if !scalar.isASCII { - return nil // bad input - } - - result.unicodeScalars.append(scalar) - outLen += 1 - - } - - var inPos = b > scalars.startIndex ? scalars.index(after: b) : scalars.startIndex - - while inPos < endIndex { - - var k = Punycode.base - var w: UInt32 = 1 - let oldi = i - - while true { - if inPos >= endIndex { - return nil // bad input - } - - let digit = Punycode.decodeDigit(scalars[inPos].value) - - inPos = scalars.index(after: inPos) - - if digit >= Punycode.base { return nil } // bad input - if digit > (UInt32.max - i) / w { return nil } // overflow - - i += digit * w - let t = k <= bias ? Punycode.tmin : - k >= bias + Punycode.tmax ? Punycode.tmax : k - bias - - if digit < t { - break - } - - if w > UInt32.max / (Punycode.base - t) { return nil } // overflow - - w *= Punycode.base - t - - k += Punycode.base - } - - bias = Punycode.adapt(delta: i - oldi, numPoints: outLen + 1, firstTime: oldi == 0) - - if i / (outLen + 1) > UInt32.max - n { return nil } // overflow - - n += i / (outLen + 1) - i %= outLen + 1 - - let index = result.unicodeScalars.index(result.unicodeScalars.startIndex, offsetBy: Int(i)) - result.unicodeScalars.insert(UnicodeScalar(n)!, at: index) - - outLen += 1 - i += 1 - } - - return result - } - -} - -private extension String { - - var urlParts: URLParts { - let colonSlash = CharacterSet(charactersIn: ":/") - let slashQuestion = CharacterSet(charactersIn: "/?") - let s = Scanner(string: self) - var scheme = "" - var delim = "" - var host = "" - var path = "" - var username: String? - var password: String? - var fragment: String? - - if let hostOrScheme = s.shimScanUpToCharacters(from: colonSlash) { - let maybeDelim = s.shimScanCharacters(from: colonSlash) ?? "" - - if maybeDelim.hasPrefix(":") { - delim = maybeDelim - scheme = hostOrScheme - host = s.shimScanUpToCharacters(from: slashQuestion) ?? "" - } else { - path.append(hostOrScheme) - path.append(maybeDelim) - } - } else if let maybeDelim = s.shimScanString("//") { - delim = maybeDelim - - if let maybeHost = s.shimScanUpToCharacters(from: slashQuestion) { - host = maybeHost - } - } - - path.append(s.shimScanUpToString("#") ?? "") - - if s.shimScanString("#") != nil { - fragment = s.shimScanUpToCharacters(from: .newlines) ?? "" - } - - let usernamePasswordHostPort = host.components(separatedBy: "@") - - switch usernamePasswordHostPort.count { - case 1: - host = usernamePasswordHostPort[0] - case 0: - break // error - default: - let usernamePassword = usernamePasswordHostPort[0].components(separatedBy: ":") - username = usernamePassword[0] - password = usernamePassword.count > 1 ? usernamePassword[1] : nil - host = usernamePasswordHostPort[1] - } - - return URLParts(scheme: scheme, delim: delim, host: host, pathAndQuery: path, username: username, password: password, fragment: fragment) - } - - enum UTS46MapError: Error { - /// A disallowed codepoint was found in the string. - case disallowedCodepoint(scalar: UnicodeScalar) - } - - /// Perform a single-pass mapping using UTS #46. - /// - /// - Returns: The mapped string. - /// - Throws: `UTS46Error`. - func mapUTS46() throws -> String { - try UTS46.loadIfNecessary() - - var result = "" - - for scalar in self.unicodeScalars { - if UTS46.disallowedCharacters.contains(scalar) { - throw UTS46MapError.disallowedCodepoint(scalar: scalar) - } - - if UTS46.ignoredCharacters.contains(scalar) { - continue - } - - if let mapped = UTS46.characterMap[scalar.value] { - result.append(mapped) - } else { - result.unicodeScalars.append(scalar) - } - } - - return result - } - - var isValidLabel: Bool { - guard self.precomposedStringWithCanonicalMapping.unicodeScalars.elementsEqual(self.unicodeScalars) else { return false } - - guard (try? self.mapUTS46()) != nil else { return false } - - if let category = self.unicodeScalars.first?.properties.generalCategory { - if category == .nonspacingMark || category == .spacingMark || category == .enclosingMark { return false } - } - - return self.hasValidJoiners - } - - /// Whether a string's joiners (if any) are valid according to IDNA 2008 ContextJ. - /// - /// See [RFC 5892, Appendix A.1 and A.2](https://tools.ietf.org/html/rfc5892#appendix-A). - var hasValidJoiners: Bool { - try! UTS46.loadIfNecessary() - - let scalars = self.unicodeScalars - - for index in scalars.indices { - let scalar = scalars[index] - - if scalar.value == 0x200C { // Zero-width non-joiner - if index == scalars.indices.first { return false } - - var subindex = scalars.index(before: index) - var previous = scalars[subindex] - - if previous.properties.canonicalCombiningClass == .virama { continue } - - while true { - guard let joiningType = UTS46.joiningTypes[previous.value] else { return false } - - if joiningType == .transparent { - if subindex == scalars.startIndex { - return false - } - - subindex = scalars.index(before: subindex) - previous = scalars[subindex] - } else if joiningType == .dual || joiningType == .left { - break - } else { - return false - } - } - - subindex = scalars.index(after: index) - var next = scalars[subindex] - - while true { - if subindex == scalars.endIndex { - return false - } - - guard let joiningType = UTS46.joiningTypes[next.value] else { return false } - - if joiningType == .transparent { - subindex = scalars.index(after: index) - next = scalars[subindex] - } else if joiningType == .right || joiningType == .dual { - break - } else { - return false - } - } - } else if scalar.value == 0x200D { // Zero-width joiner - if index == scalars.startIndex { return false } - - let subindex = scalars.index(before: index) - let previous = scalars[subindex] - - if previous.properties.canonicalCombiningClass != .virama { return false } - } - } - - return true - } - -} - -private enum Punycode { - static let base = UInt32(36) - static let tmin = UInt32(1) - static let tmax = UInt32(26) - static let skew = UInt32(38) - static let damp = UInt32(700) - static let initialBias = UInt32(72) - static let initialN = UInt32(0x80) - static let delimiter: Character = "-" - - static func decodeDigit(_ cp: UInt32) -> UInt32 { - return cp &- 48 < 10 ? cp &- 22 : cp &- 65 < 26 ? cp &- 65 : - cp &- 97 < 26 ? cp &- 97 : Self.base - } - - static func encodeDigit(_ d: UInt32, flag: Bool) -> UInt32 { - return d + 22 + 75 * UInt32(d < 26 ? 1 : 0) - ((flag ? 1 : 0) << 5) - } - - static let maxint = UInt32.max - - static func adapt(delta: UInt32, numPoints: UInt32, firstTime: Bool) -> UInt32 { - - var delta = delta - - delta = firstTime ? delta / Self.damp : delta >> 1 - delta += delta / numPoints - - var k: UInt32 = 0 - - while delta > ((Self.base - Self.tmin) * Self.tmax) / 2 { - delta /= Self.base - Self.tmin - k += Self.base - } - - return k + (Self.base - Self.tmin + 1) * delta / (delta + Self.skew) - } -} - -private struct URLParts { - var scheme: String - var delim: String - var host: String - var pathAndQuery: String - - var username: String? - var password: String? - var fragment: String? -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46+Loading.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46+Loading.swift deleted file mode 100644 index 3c6f98144..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46+Loading.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// UTS46+Loading.swift -// icumap2code -// -// Created by Nate Weaver on 2020-05-08. -// - -import Foundation -import Compression - -extension UTS46 { - - private static func parseHeader(from data: Data) throws -> Header? { - let headerData = data.prefix(8) - - guard headerData.count == 8 else { throw UTS46Error.badSize } - - return Header(rawValue: headerData) - } - - static func load(from url: URL) throws { - let fileData = try Data(contentsOf: url) - - guard let header = try? parseHeader(from: fileData) else { return } - - guard header.version == 1 else { throw UTS46Error.unknownVersion } - - let offset = header.dataOffset - - guard fileData.count > offset else { throw UTS46Error.badSize } - - let compressedData = fileData[offset...] - - guard let data = self.decompress(data: compressedData, algorithm: header.compression) else { - throw UTS46Error.decompressionError - } - - var index = 0 - - while index < data.count { - let marker = data[index] - - index += 1 - - switch marker { - case Marker.characterMap: - index = parseCharacterMap(from: data, start: index) - case Marker.ignoredCharacters: - index = parseIgnoredCharacters(from: data, start: index) - case Marker.disallowedCharacters: - index = parseDisallowedCharacters(from: data, start: index) - case Marker.joiningTypes: - index = parseJoiningTypes(from: data, start: index) - default: - throw UTS46Error.badMarker - } - } - - isLoaded = true - } - - static var bundle: Bundle { - #if SWIFT_PACKAGE - return Bundle.module - #else - return Bundle(for: Self.self) - #endif - } - - static func loadIfNecessary() throws { - guard !isLoaded else { return } - guard let url = Self.bundle.url(forResource: "uts46", withExtension: nil) else { throw CocoaError(.fileNoSuchFile) } - - try load(from: url) - } - - private static func decompress(data: Data, algorithm: CompressionAlgorithm?) -> Data? { - - guard let rawAlgorithm = algorithm?.rawAlgorithm else { return data } - - let capacity = 131_072 // 128 KB - let destinationBuffer = UnsafeMutablePointer.allocate(capacity: capacity) - - let decompressed = data.withUnsafeBytes { (rawBuffer) -> Data? in - let bound = rawBuffer.bindMemory(to: UInt8.self) - let decodedCount = compression_decode_buffer(destinationBuffer, capacity, bound.baseAddress!, rawBuffer.count, nil, rawAlgorithm) - - if decodedCount == 0 || decodedCount == capacity { - return nil - } - - return Data(bytes: destinationBuffer, count: decodedCount) - } - - return decompressed - } - - private static func parseCharacterMap(from data: Data, start: Int) -> Int { - characterMap.removeAll() - var index = start - - main: while index < data.count { - var accumulator = Data() - - while data[index] != Marker.sequenceTerminator { - if data[index] > Marker.min { break main } - - accumulator.append(data[index]) - index += 1 - } - - let str = String(data: accumulator, encoding: .utf8)! - - // FIXME: throw an error here. - guard str.count > 0 else { continue } - - let codepoint = str.unicodeScalars.first!.value - - characterMap[codepoint] = String(str.unicodeScalars.dropFirst()) - - index += 1 - } - - return index - } - - private static func parseRanges(from: String) -> [ClosedRange]? { - guard from.unicodeScalars.count % 2 == 0 else { return nil } - - var ranges = [ClosedRange]() - var first: UnicodeScalar? - - for (index, scalar) in from.unicodeScalars.enumerated() { - if index % 2 == 0 { - first = scalar - } else if let first = first { - ranges.append(first...scalar) - } - } - - return ranges - } - - static func parseCharacterSet(from data: Data, start: Int) -> (index: Int, charset: CharacterSet?) { - var index = start - var accumulator = Data() - - while index < data.count, data[index] < Marker.min { - accumulator.append(data[index]) - index += 1 - } - - let str = String(data: accumulator, encoding: .utf8)! - - guard let ranges = parseRanges(from: str) else { - return (index: index, charset: nil) - } - - var charset = CharacterSet() - - for range in ranges { - charset.insert(charactersIn: range) - } - - return (index: index, charset: charset) - } - - static func parseIgnoredCharacters(from data: Data, start: Int) -> Int { - let (index, charset) = parseCharacterSet(from: data, start: start) - - if let charset = charset { - ignoredCharacters = charset - } - - return index - } - - static func parseDisallowedCharacters(from data: Data, start: Int) -> Int { - let (index, charset) = parseCharacterSet(from: data, start: start) - - if let charset = charset { - disallowedCharacters = charset - } - - return index - } - - static func parseJoiningTypes(from data: Data, start: Int) -> Int { - var index = start - joiningTypes.removeAll() - - main: while index < data.count, data[index] < Marker.min { - var accumulator = Data() - - while index < data.count { - if data[index] > Marker.min { break main } - accumulator.append(data[index]) - - index += 1 - } - - let str = String(data: accumulator, encoding: .utf8)! - - var type: JoiningType? - var first: UnicodeScalar? - - for scalar in str.unicodeScalars { - if scalar.isASCII { - type = JoiningType(rawValue: Character(scalar)) - } else if let type = type { - if first == nil { - first = scalar - } else { - for value in first!.value...scalar.value { - joiningTypes[value] = type - } - - first = nil - } - } - } - } - - return index - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift deleted file mode 100644 index fa7a2faaa..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// UTS46.swift -// PunyCocoa Swift -// -// Created by Nate Weaver on 2020-03-29. -// - -import Foundation -import Compression - -/// UTS46 mapping. -/// -/// Storage file format. Codepoints are stored UTF-8-encoded. -/// -/// All multibyte integers are little-endian. -/// -/// Header: -/// -/// +--------------+---------+---------+---------+ -/// | 6 bytes | 1 byte | 1 byte | 4 bytes | -/// +--------------+---------+---------+---------+ -/// | magic number | version | flags | crc32 | -/// +--------------+---------+---------+---------+ -/// -/// - `magic number`: `"UTS#46"` (`0x55 0x54 0x53 0x23 0x34 0x36`). -/// - `version`: format version (1 byte; currently `0x01`). -/// - `flags`: Bitfield: -/// -/// +-----+-----+-----+-----+-----+-----+-----+-----+ -/// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | -/// +-----+-----+-----+-----+-----+-----+-----+-----+ -/// | currently unused | crc | compression | -/// +-----+-----+-----+-----+-----+-----+-----+-----+ -/// -/// - `crc`: Contains a CRC32 of the data after the header. -/// - `compression`: compression mode of the data. -/// Currently identical to NSData's compression constants + 1: -/// -/// - 0: no compression -/// - 1: LZFSE -/// - 2: LZ4 -/// - 3: LZMA -/// - 4: ZLIB -/// -/// - `crc32`: CRC32 of the (possibly compressed) data. Implementations can skip -/// parsing this unless data integrity is an issue. -/// -/// The data section is a collection of data blocks of the format -/// -/// [marker][section data] ... -/// -/// Section data formats: -/// -/// If marker is `characterMap`: -/// -/// [codepoint][mapped-codepoint ...][null] ... -/// -/// If marker is `disallowedCharacters` or `ignoredCharacters`: -/// -/// [codepoint-range] ... -/// -/// If marker is `joiningTypes`: -/// -/// [type][[codepoint-range] ...] -/// -/// where `type` is one of `C`, `D`, `L`, `R`, or `T`. -/// -/// `codepoint-range`: two codepoints, marking the first and last codepoints of a -/// closed range. Single-codepoint ranges have the same start and end codepoint. -/// -class UTS46 { - - static var characterMap: [UInt32: String] = [:] - static var ignoredCharacters: CharacterSet = [] - static var disallowedCharacters: CharacterSet = [] - static var joiningTypes = [UInt32: JoiningType]() - - static var isLoaded = false - - enum Marker { - static let characterMap = UInt8.max - static let ignoredCharacters = UInt8.max - 1 - static let disallowedCharacters = UInt8.max - 2 - static let joiningTypes = UInt8.max - 3 - - static let min = UInt8.max - 10 // No valid UTF-8 byte can fall here. - - static let sequenceTerminator: UInt8 = 0 - } - - enum JoiningType: Character { - case causing = "C" - case dual = "D" - case right = "R" - case left = "L" - case transparent = "T" - } - - enum UTS46Error: Error { - case badSize - case compressionError - case decompressionError - case badMarker - case unknownVersion - } - - /// Identical values to `NSData.CompressionAlgorithm + 1`. - enum CompressionAlgorithm: UInt8 { - case none = 0 - case lzfse = 1 - case lz4 = 2 - case lzma = 3 - case zlib = 4 - - var rawAlgorithm: compression_algorithm? { - switch self { - case .lzfse: - return COMPRESSION_LZFSE - case .lz4: - return COMPRESSION_LZ4 - case .lzma: - return COMPRESSION_LZMA - case .zlib: - return COMPRESSION_ZLIB - default: - return nil - } - } - } - - struct Header: RawRepresentable, CustomDebugStringConvertible { - typealias RawValue = [UInt8] - - var rawValue: [UInt8] { - let value = Self.signature + [version, flags.rawValue] - assert(value.count == 8) - return value - } - - private static let compressionMask: UInt8 = 0x07 - private static let signature: [UInt8] = Array("UTS#46".utf8) - - private struct Flags: RawRepresentable { - var rawValue: UInt8 { - return (hasCRC ? hasCRCMask : 0) | compression.rawValue - } - - var hasCRC: Bool - var compression: CompressionAlgorithm - - private let hasCRCMask: UInt8 = 1 << 3 - private let compressionMask: UInt8 = 0x7 - - init(rawValue: UInt8) { - hasCRC = rawValue & hasCRCMask != 0 - let compressionBits = rawValue & compressionMask - - compression = CompressionAlgorithm(rawValue: compressionBits) ?? .none - } - - init(compression: CompressionAlgorithm = .none, hasCRC: Bool = false) { - self.compression = compression - self.hasCRC = hasCRC - } - } - - let version: UInt8 - private var flags: Flags - var hasCRC: Bool { flags.hasCRC } - var compression: CompressionAlgorithm { flags.compression } - var dataOffset: Int { 8 + (flags.hasCRC ? 4 : 0) } - - init?(rawValue: T) where T.Index == Int { - guard rawValue.count == 8 else { return nil } - guard rawValue.prefix(Self.signature.count).elementsEqual(Self.signature) else { return nil } - - version = rawValue[rawValue.index(rawValue.startIndex, offsetBy: 6)] - flags = Flags(rawValue: rawValue[rawValue.index(rawValue.startIndex, offsetBy: 7)]) - } - - init(compression: CompressionAlgorithm = .none, hasCRC: Bool = false) { - self.version = 1 - self.flags = Flags(compression: compression, hasCRC: hasCRC) - } - - var debugDescription: String { "has CRC: \(hasCRC); compression: \(String(describing: compression))" } - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/uts46 b/Modules/RSWeb/Sources/RSWeb/UTS46/uts46 deleted file mode 100644 index 101001987..000000000 Binary files a/Modules/RSWeb/Sources/RSWeb/UTS46/uts46 and /dev/null differ diff --git a/iOS/Add/AddFeedViewController.swift b/iOS/Add/AddFeedViewController.swift index fbaafb39e..eca51f897 100644 --- a/iOS/Add/AddFeedViewController.swift +++ b/iOS/Add/AddFeedViewController.swift @@ -81,7 +81,7 @@ class AddFeedViewController: UITableViewController { let urlString = urlTextField.text ?? "" let normalizedURLString = urlString.normalizedURL - guard !normalizedURLString.isEmpty, let url = URL(unicodeString: normalizedURLString) else { + guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else { return } diff --git a/iOS/Inspector/WebFeedInspectorViewController.swift b/iOS/Inspector/WebFeedInspectorViewController.swift index 91e829f58..e7a5815c0 100644 --- a/iOS/Inspector/WebFeedInspectorViewController.swift +++ b/iOS/Inspector/WebFeedInspectorViewController.swift @@ -45,8 +45,8 @@ class WebFeedInspectorViewController: UITableViewController { alwaysShowReaderViewSwitch.setOn(webFeed.isArticleExtractorAlwaysOn ?? false, animated: false) - homePageLabel.text = webFeed.homePageURL?.decodedURLString - feedURLLabel.text = webFeed.url.decodedURLString + homePageLabel.text = webFeed.homePageURL + feedURLLabel.text = webFeed.url NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)