mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'feature/delete-unused-code'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,8 +19,8 @@ let package = Package(
|
||||
targets: [
|
||||
.target(
|
||||
name: "RSDatabase",
|
||||
dependencies: ["RSDatabaseObjC"],
|
||||
exclude: ["ODB/README.markdown"]),
|
||||
dependencies: ["RSDatabaseObjC"]
|
||||
),
|
||||
.target(
|
||||
name: "RSDatabaseObjC",
|
||||
dependencies: []
|
||||
|
||||
@@ -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;
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
35
Modules/RSDatabase/Tests/RSDatabaseTests/DatabaseTests.swift
Normal file
35
Modules/RSDatabase/Tests/RSDatabaseTests/DatabaseTests.swift
Normal 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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"])
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))" }
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user