Merge branch 'feature/delete-unused-code'

This commit is contained in:
Brent Simmons
2025-04-24 16:28:45 -07:00
29 changed files with 54 additions and 2504 deletions

View File

@@ -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

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -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())

View File

@@ -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
}

View File

@@ -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()
}
}
}

View File

@@ -19,8 +19,8 @@ let package = Package(
targets: [
.target(
name: "RSDatabase",
dependencies: ["RSDatabaseObjC"],
exclude: ["ODB/README.markdown"]),
dependencies: ["RSDatabaseObjC"]
),
.target(
name: "RSDatabaseObjC",
dependencies: []

View File

@@ -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.
// Its 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.
/// Its 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 theres 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;
"""
}

View File

@@ -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 }
}

View File

@@ -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, its ignored: "root" is implied.
An empty array or ["root"] refers to the root table.
A path does not necessarily point to something that exists. Its 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.
/// Wont 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: dont 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
}
}

View File

@@ -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 youre 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
}
}

View File

@@ -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 {
// Dont 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
}
}

View File

@@ -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<ODBTable> {
guard let rs: FMResultSet = database.executeQuery("select * from odb_tables where parent_id = ?", withArgumentsIn: [table.uniqueID]) else {
return Set<ODBTable>()
}
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)
}
}

View File

@@ -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: theyre 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
}
}
}

View File

@@ -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
}
}

View File

@@ -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<ODBValueObject> {
guard let rs = database.rs_selectRowsWhereKey(Key.parentID, equalsValue: table.uniqueID, tableName: name) else {
return Set<ODBValueObject>()
}
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)
}
}

View File

@@ -1,149 +0,0 @@
# ODB
**NOTE**: This all has been excluded from building. Its a work in progress, not ready for use.
ODB stands for Object Database.
“Object” doesnt mean object in the object-oriented programming sense — it just means *thing*.
Think of the ODB as a nested Dictionary thats *persistent*. Its schema-less. Tables (which are like dictionaries) can contain other tables. Its 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.
Its 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 its 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 Frontiers 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 Winers.)
## How to use it
### Create an ODB
`let odb = ODB(filepath: somePath)` creates a new ODB for that path. If theres an existing database on disk, it uses that one. Otherwise it creates a new one.
### Ensuring that a table exists
Lets say youre writing an RSS reader, and you want to make sure theres 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 its 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 dont exist.
Operations referencing `ODBTable` and `ODBValueObject` must be enclosed in an `ODB.perform` block. This is for thread safety. If you dont 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`: its a protocol.
### Setting a value
Lets 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 youve 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 doesnt exist, then it gets created.
(Yes, this would be so much easier in a scripting language. Youd 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, its good practice to use that ability sparingly. When you can break things down into simple primitive types, thats best. Treating an entire table, with multiple stored values, as a unit is often the way to go. But not always.
### Getting a value
Lets say you want to get back the edited name of the feed. Youd 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. Its an immutable struct with no connection to the database. But in reality youd probably write the above code more like this:
var name: String?
ODB.perform {
name = path.value? as? String
}
Its totally a-okay to use Swifts built-in types this way instead of checking the ODBValues `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 feeds name, and now you want to delete `RSS.feeds.[feedID].editedName` — given the path, youd 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 wont use this directly all that often. It wraps an `ODBValue`, which youll use way more often. The useful API for `ODBValueObject` is almost entirely in `ODBObject`.
## Notes
### The root table
The one table you cant delete is the root table — every ODB has a top-level table named `root`. You dont usually specify `root` as the first part of a path, but you could. Its implied.
A path like `["RSS", "feeds"]` is precisely the same as `["root", "RSS", "feeds"]` — theyre interchangeable paths.
### Case-sensitivity
Frontiers object database was case-insensitive: you could refer to the "feeds" table as the "FEeDs" table — it would be the same thing.
While I dont know this for sure, I assume this was because the Macs file system is also case-insensitive. This was considered one of the user-friendly things about Macs.
Were 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 machines locale actually is. This is something to be aware of.
### Caching and Performance
The database is cached in memory as it is used. A tables children are not read into memory until referenced.
For objects already in memory, reads are fast since theres no need to query the SQLite database.
If this caching becomes a problem in production use — if it tends to use too much memory — well 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 its 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 dont exist. The database can change while you hold an `ODBPath` reference, and thats okay: its 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 doesnt 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 apps 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, its necessary to use a lock when working with these — thats 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.

View File

@@ -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.
}
}
}

View File

@@ -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)
}
}

View File

@@ -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"])
]
)

View File

@@ -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 couldnt construct a URL.")
}

View File

@@ -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)))
}
}
}

View File

@@ -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?
}
}
}

View File

@@ -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?
}

View File

@@ -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<UInt8>.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<UnicodeScalar>]? {
guard from.unicodeScalars.count % 2 == 0 else { return nil }
var ranges = [ClosedRange<UnicodeScalar>]()
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
}
}

View File

@@ -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?<T: DataProtocol>(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))" }
}
}

View File

@@ -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
}

View File

@@ -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)