mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-08 08:48:39 +09:00
319 lines
11 KiB
Swift
319 lines
11 KiB
Swift
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import Foundation
|
|
import Shared
|
|
|
|
private let log = Logger.syncLogger
|
|
|
|
// MARK: - Defining a tree structure for syncability.
|
|
public enum BookmarkTreeNode: Comparable {
|
|
indirect case folder(guid: GUID, children: [BookmarkTreeNode])
|
|
case nonFolder(guid: GUID)
|
|
case unknown(guid: GUID)
|
|
|
|
// Because shared associated values between enum cases aren't possible.
|
|
public var recordGUID: GUID {
|
|
switch self {
|
|
case let .folder(guid, _):
|
|
return guid
|
|
case let .nonFolder(guid):
|
|
return guid
|
|
case let .unknown(guid):
|
|
return guid
|
|
}
|
|
}
|
|
|
|
public var isRoot: Bool {
|
|
return BookmarkRoots.All.contains(self.recordGUID)
|
|
}
|
|
|
|
public var isUnknown: Bool {
|
|
if case .unknown = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
public var children: [BookmarkTreeNode]? {
|
|
if case let .folder(_, children) = self {
|
|
return children
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func hasChildList(_ nodes: [BookmarkTreeNode]) -> Bool {
|
|
if case let .folder(_, ours) = self {
|
|
return ours.elementsEqual(nodes, by: { $0.recordGUID == $1.recordGUID })
|
|
}
|
|
return false
|
|
}
|
|
|
|
public func hasSameChildListAs(_ other: BookmarkTreeNode) -> Bool {
|
|
if case let .folder(_, ours) = self {
|
|
if case let .folder(_, theirs) = other {
|
|
return ours.elementsEqual(theirs, by: { $0.recordGUID == $1.recordGUID })
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Returns false for unknowns.
|
|
public func isSameTypeAs(_ other: BookmarkTreeNode) -> Bool {
|
|
switch self {
|
|
case .folder:
|
|
if case .folder = other {
|
|
return true
|
|
}
|
|
case .nonFolder:
|
|
if case .nonFolder = other {
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func == (lhs: BookmarkTreeNode, rhs: BookmarkTreeNode) -> Bool {
|
|
switch lhs {
|
|
case let .folder(guid, children):
|
|
if case let .folder(rguid, rchildren) = rhs {
|
|
return guid == rguid && children == rchildren
|
|
}
|
|
return false
|
|
case let .nonFolder(guid):
|
|
if case let .nonFolder(rguid) = rhs {
|
|
return guid == rguid
|
|
}
|
|
return false
|
|
case let .unknown(guid):
|
|
if case let .unknown(rguid) = rhs {
|
|
return guid == rguid
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func < (lhs: BookmarkTreeNode, rhs: BookmarkTreeNode) -> Bool {
|
|
return lhs.recordGUID < rhs.recordGUID
|
|
}
|
|
|
|
typealias StructureRow = (parent: GUID, child: GUID, type: BookmarkNodeType?)
|
|
|
|
// This is really a forest, not a tree: it can have multiple 'subtrees'
|
|
// and carries a collection of associated values.
|
|
public struct BookmarkTree {
|
|
// Records with no parents.
|
|
public let subtrees: [BookmarkTreeNode]
|
|
|
|
// Record GUID -> record.
|
|
public let lookup: [GUID: BookmarkTreeNode]
|
|
|
|
// Child GUID -> parent GUID.
|
|
public let parents: [GUID: GUID]
|
|
|
|
// Records that appear in 'lookup' because they're modified, but aren't present
|
|
// in 'subtrees' because their parent didn't change.
|
|
public let orphans: Set<GUID>
|
|
|
|
// Records that have been deleted.
|
|
public let deleted: Set<GUID>
|
|
|
|
// Every record that's changed but not deleted.
|
|
public let modified: Set<GUID>
|
|
|
|
// Nodes that are present in this tree but aren't present in the source.
|
|
// In practical terms, this will be roots that we pretend exist in
|
|
// the mirror for purposes of three-way merging.
|
|
public let virtual: Set<GUID>
|
|
|
|
// Accessor for all top-level folders' GUIDs.
|
|
public var subtreeGUIDs: Set<GUID> {
|
|
return Set(self.subtrees.map { $0.recordGUID })
|
|
}
|
|
|
|
public var isEmpty: Bool {
|
|
return self.subtrees.isEmpty && self.deleted.isEmpty
|
|
}
|
|
|
|
public static func emptyTree() -> BookmarkTree {
|
|
return BookmarkTree(subtrees: [], lookup: [:], parents: [:], orphans: Set<GUID>(), deleted: Set<GUID>(), modified: Set<GUID>(), virtual: Set<GUID>())
|
|
}
|
|
|
|
public static func emptyMirrorTree() -> BookmarkTree {
|
|
return mappingsToTreeForStructureRows([], withNonFoldersAndEmptyFolders: [], withDeletedRecords: Set(), modifiedRecords: Set(), alwaysIncludeRoots: true)
|
|
}
|
|
|
|
public func includesOrDeletesNode(_ node: BookmarkTreeNode) -> Bool {
|
|
return self.includesOrDeletesGUID(node.recordGUID)
|
|
}
|
|
|
|
public func includesNode(_ node: BookmarkTreeNode) -> Bool {
|
|
return self.includesGUID(node.recordGUID)
|
|
}
|
|
|
|
public func includesOrDeletesGUID(_ guid: GUID) -> Bool {
|
|
return self.includesGUID(guid) || self.deleted.contains(guid)
|
|
}
|
|
|
|
public func includesGUID(_ guid: GUID) -> Bool {
|
|
return self.lookup[guid] != nil
|
|
}
|
|
|
|
public func find(_ guid: GUID) -> BookmarkTreeNode? {
|
|
return self.lookup[guid]
|
|
}
|
|
|
|
public func find(_ node: BookmarkTreeNode) -> BookmarkTreeNode? {
|
|
return self.find(node.recordGUID)
|
|
}
|
|
|
|
/**
|
|
* True if there is one subtree, and it's the Root, when overlayed.
|
|
* We assume that the mirror will always be consistent, so what
|
|
* this really means is that every subtree in this tree is *present*
|
|
* in the comparison tree, or is itself rooted in a known root.
|
|
*
|
|
* In a fully rooted tree there can be no orphans; if our partial tree
|
|
* includes orphans, they must be known by the comparison tree.
|
|
*/
|
|
public func isFullyRootedIn(_ tree: BookmarkTree) -> Bool {
|
|
// We don't compare against tree.deleted, because you can't *undelete*.
|
|
return self.orphans.every(tree.includesGUID) &&
|
|
self.subtrees.every { subtree in
|
|
tree.includesNode(subtree) || subtree.isRoot
|
|
}
|
|
}
|
|
|
|
// If this tree contains the root, return it.
|
|
public var root: BookmarkTreeNode? {
|
|
return self.find(BookmarkRoots.RootGUID)
|
|
}
|
|
|
|
// Recursively process an input set of structure pairs to yield complete subtrees,
|
|
// assembling those subtrees to make a minimal set of trees.
|
|
static func mappingsToTreeForStructureRows(_ mappings: [StructureRow], withNonFoldersAndEmptyFolders nonFoldersAndEmptyFolders: [BookmarkTreeNode], withDeletedRecords deleted: Set<GUID>, modifiedRecords modified: Set<GUID>, alwaysIncludeRoots: Bool) -> BookmarkTree {
|
|
// Accumulate.
|
|
var nodes: [GUID: BookmarkTreeNode] = [:]
|
|
var parents: [GUID: GUID] = [:]
|
|
var remainingFolders = Set<GUID>()
|
|
|
|
// `tops` is the collection of things that we think are the roots of subtrees (until
|
|
// we're proved wrong). We add GUIDs here when we don't know their parents; if we get to
|
|
// the end and they're still here, they're roots.
|
|
var tops = Set<GUID>()
|
|
var notTops = Set<GUID>()
|
|
var orphans = Set<GUID>()
|
|
var virtual = Set<GUID>()
|
|
|
|
// We can't immediately build the final tree, because we need to do it bottom-up!
|
|
// So store structure, which we can figure out flat.
|
|
var pseudoTree: [GUID: [GUID]] = mappings.groupBy({ $0.parent }, transformer: { $0.child })
|
|
|
|
// Deal with the ones that are non-structural first.
|
|
nonFoldersAndEmptyFolders.forEach { node in
|
|
let guid = node.recordGUID
|
|
nodes[guid] = node
|
|
|
|
switch node {
|
|
case .folder:
|
|
// If we end up here, it's because this folder is empty, and it won't
|
|
// appear in structure. Assert to make sure that's true!
|
|
assert(pseudoTree[guid] == nil)
|
|
pseudoTree[guid] = []
|
|
|
|
// It'll be a top unless we find it as a child in the structure somehow.
|
|
tops.insert(guid)
|
|
default:
|
|
orphans.insert(guid)
|
|
}
|
|
}
|
|
|
|
// Precompute every leaf node.
|
|
mappings.forEach { row in
|
|
parents[row.child] = row.parent
|
|
remainingFolders.insert(row.parent)
|
|
tops.insert(row.parent)
|
|
|
|
// None of the children we've seen can be top, so remove them.
|
|
notTops.insert(row.child)
|
|
|
|
if let type = row.type {
|
|
switch type {
|
|
case .folder:
|
|
// The child is itself a folder.
|
|
remainingFolders.insert(row.child)
|
|
default:
|
|
nodes[row.child] = BookmarkTreeNode.nonFolder(guid: row.child)
|
|
}
|
|
} else {
|
|
// This will be the case if we've shadowed a folder; we indirectly reference the original rows.
|
|
nodes[row.child] = BookmarkTreeNode.unknown(guid: row.child)
|
|
}
|
|
}
|
|
|
|
// When we build the mirror, we always want to pretend it has our stock roots.
|
|
// This gives us our shared basis from which to merge.
|
|
// Doing it here means we don't need to protect the mirror database table.
|
|
if alwaysIncludeRoots {
|
|
func setVirtual(_ guid: GUID) {
|
|
if !remainingFolders.contains(guid) && nodes[guid] == nil {
|
|
virtual.insert(guid)
|
|
}
|
|
}
|
|
|
|
// Note that we don't check whether the input already contained the roots; we
|
|
// never change them, so it's safe to do this unconditionally.
|
|
setVirtual(BookmarkRoots.RootGUID)
|
|
BookmarkRoots.RootChildren.forEach {
|
|
setVirtual($0)
|
|
}
|
|
|
|
pseudoTree[BookmarkRoots.RootGUID] = BookmarkRoots.RootChildren
|
|
tops.insert(BookmarkRoots.RootGUID)
|
|
notTops.formUnion(Set(BookmarkRoots.RootChildren))
|
|
remainingFolders.formUnion(BookmarkRoots.All)
|
|
BookmarkRoots.RootChildren.forEach {
|
|
parents[$0] = BookmarkRoots.RootGUID
|
|
}
|
|
}
|
|
|
|
tops.subtract(notTops)
|
|
orphans.subtract(notTops)
|
|
|
|
// Recursive. (Not tail recursive, but trees shouldn't be deep enough to blow the stack….)
|
|
@discardableResult func nodeForGUID(_ guid: GUID) -> BookmarkTreeNode {
|
|
if let already = nodes[guid] {
|
|
return already
|
|
}
|
|
|
|
if !remainingFolders.contains(guid) {
|
|
let node = BookmarkTreeNode.unknown(guid: guid)
|
|
nodes[guid] = node
|
|
return node
|
|
}
|
|
|
|
// Removing these eagerly prevents infinite recursion in the case of a cycle.
|
|
let childGUIDs = pseudoTree[guid] ?? []
|
|
pseudoTree.removeValue(forKey: guid)
|
|
remainingFolders.remove(guid)
|
|
|
|
let node = BookmarkTreeNode.folder(guid: guid, children: childGUIDs.map(nodeForGUID))
|
|
nodes[guid] = node
|
|
return node
|
|
}
|
|
|
|
// Process every record.
|
|
// Do the not-tops first: shallower recursion.
|
|
notTops.forEach({ nodeForGUID($0) })
|
|
|
|
let subtrees = tops.map(nodeForGUID) // These will all be folders.
|
|
|
|
// Whatever we're left with in `tops` is the set of records for which we
|
|
// didn't process a parent.
|
|
return BookmarkTree(subtrees: subtrees, lookup: nodes, parents: parents, orphans: orphans, deleted: deleted, modified: modified, virtual: virtual)
|
|
}
|
|
}
|