]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
iOS: Apply runtime configuration by diff-ing
authorRoopesh Chander <roop@roopc.net>
Fri, 1 Feb 2019 11:36:42 +0000 (17:06 +0530)
committerRoopesh Chander <roop@roopc.net>
Sat, 2 Feb 2019 13:52:01 +0000 (19:22 +0530)
And apply the diff on the tableView as insert/remove/reloads.

Signed-off-by: Roopesh Chander <roop@roopc.net>
WireGuard/WireGuard/UI/TunnelViewModel.swift
WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift

index 886703eda30dae4af670b230c8b4b827d7f2c9be..fcbaef3bea699d3fc32426347605aabe5e200269 100644 (file)
@@ -6,7 +6,7 @@ import Foundation
 //swiftlint:disable:next type_body_length
 class TunnelViewModel {
 
-    enum InterfaceField {
+    enum InterfaceField: CaseIterable {
         case name
         case privateKey
         case publicKey
@@ -34,7 +34,7 @@ class TunnelViewModel {
         .generateKeyPair
     ]
 
-    enum PeerField {
+    enum PeerField: CaseIterable {
         case publicKey
         case preSharedKey
         case endpoint
@@ -68,6 +68,18 @@ class TunnelViewModel {
 
     static let keyLengthInBase64 = 44
 
+    struct ChangeHandlers {
+        enum FieldChange {
+            case added
+            case removed
+            case modified
+        }
+        var interfaceChanged: ([InterfaceField: FieldChange]) -> Void
+        var peerChangedAt: (Int, [PeerField: FieldChange]) -> Void
+        var peersRemovedAt: ([Int]) -> Void
+        var peersInsertedAt: ([Int]) -> Void
+    }
+
     class InterfaceData {
         var scratchpad = [InterfaceField: String]()
         var fieldsWithError = Set<InterfaceField>()
@@ -106,6 +118,11 @@ class TunnelViewModel {
         func populateScratchpad() {
             guard let config = validatedConfiguration else { return }
             guard let name = validatedName else { return }
+            scratchpad = TunnelViewModel.InterfaceData.createScratchPad(from: config, name: name)
+        }
+
+        private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
+            var scratchpad = [InterfaceField: String]()
             scratchpad[.name] = name
             scratchpad[.privateKey] = config.privateKey.base64EncodedString()
             scratchpad[.publicKey] = config.publicKey.base64EncodedString()
@@ -121,6 +138,7 @@ class TunnelViewModel {
             if !config.dns.isEmpty {
                 scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
             }
+            return scratchpad
         }
 
         //swiftlint:disable:next cyclomatic_complexity function_body_length
@@ -199,6 +217,32 @@ class TunnelViewModel {
                 return !self[field].isEmpty
             }
         }
+
+        func applyConfiguration(other: InterfaceConfiguration, otherName: String, changeHandler: ([InterfaceField: ChangeHandlers.FieldChange]) -> Void) {
+            if scratchpad.isEmpty {
+                populateScratchpad()
+            }
+            let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
+            var changes = [InterfaceField: ChangeHandlers.FieldChange]()
+            for field in InterfaceField.allCases {
+                switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
+                case ("", ""):
+                    break
+                case ("", _):
+                    changes[field] = .added
+                case (_, ""):
+                    changes[field] = .removed
+                case (let this, let other):
+                    if this != other {
+                        changes[field] = .modified
+                    }
+                }
+            }
+            scratchpad = otherScratchPad
+            if !changes.isEmpty {
+                changeHandler(changes)
+            }
+        }
     }
 
     class PeerData {
@@ -206,6 +250,15 @@ class TunnelViewModel {
         var scratchpad = [PeerField: String]()
         var fieldsWithError = Set<PeerField>()
         var validatedConfiguration: PeerConfiguration?
+        var publicKey: Data? {
+            if let validatedConfiguration = validatedConfiguration {
+                return validatedConfiguration.publicKey
+            }
+            if let scratchPadPublicKey = scratchpad[.publicKey] {
+                return Data(base64Encoded: scratchPadPublicKey)
+            }
+            return nil
+        }
 
         private(set) var shouldAllowExcludePrivateIPsControl = false
         private(set) var shouldStronglyRecommendDNS = false
@@ -241,6 +294,12 @@ class TunnelViewModel {
 
         func populateScratchpad() {
             guard let config = validatedConfiguration else { return }
+            scratchpad = TunnelViewModel.PeerData.createScratchPad(from: config)
+            updateExcludePrivateIPsFieldState()
+        }
+
+        private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
+            var scratchpad = [PeerField: String]()
             scratchpad[.publicKey] = config.publicKey.base64EncodedString()
             if let preSharedKey = config.preSharedKey {
                 scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
@@ -263,7 +322,7 @@ class TunnelViewModel {
             if let lastHandshakeTime = config.lastHandshakeTime {
                 scratchpad[.lastHandshakeTime] = prettyTimeAgo(timestamp: lastHandshakeTime)
             }
-            updateExcludePrivateIPsFieldState()
+            return scratchpad
         }
 
         //swiftlint:disable:next cyclomatic_complexity
@@ -381,6 +440,30 @@ class TunnelViewModel {
             validatedConfiguration = nil
             excludePrivateIPsValue = isOn
         }
+
+        func applyConfiguration(other: PeerConfiguration, peerIndex: Int, changeHandler: (Int, [PeerField: ChangeHandlers.FieldChange]) -> Void) {
+            if scratchpad.isEmpty {
+                populateScratchpad()
+            }
+            let otherScratchPad = PeerData.createScratchPad(from: other)
+            var changes = [PeerField: ChangeHandlers.FieldChange]()
+            for field in PeerField.allCases {
+                switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
+                case ("", ""):
+                    break
+                case ("", _):
+                    changes[field] = .added
+                case (_, ""):
+                    changes[field] = .removed
+                case (let this, let other):
+                    if this != other {
+                        changes[field] = .modified
+                    }
+                }
+            }
+            scratchpad = otherScratchPad
+            changeHandler(peerIndex, changes)
+        }
     }
 
     enum SaveResult<Configuration> {
@@ -388,8 +471,8 @@ class TunnelViewModel {
         case error(String)
     }
 
-    var interfaceData: InterfaceData
-    var peersData: [PeerData]
+    private(set) var interfaceData: InterfaceData
+    private(set) var peersData: [PeerData]
 
     init(tunnelConfiguration: TunnelConfiguration?) {
         let interfaceData = InterfaceData()
@@ -462,6 +545,55 @@ class TunnelViewModel {
             return .saved(tunnelConfiguration)
         }
     }
+
+    func applyConfiguration(other: TunnelConfiguration, changeHandlers: ChangeHandlers) {
+        // Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
+        // Change handler callbacks are processed in the following order, which is designed to work with both the
+        // UITableView way (modify - delete - insert) and the NSTableView way (indices are based on past operations):
+        //   - interfaceChanged
+        //   - peerChangedAt
+        //   - peersRemovedAt
+        //   - peersInsertedAt
+
+        interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "", changeHandler: changeHandlers.interfaceChanged)
+
+        for otherPeer in other.peers {
+            if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
+                let peerData = peersData[peersDataIndex]
+                peerData.applyConfiguration(other: otherPeer, peerIndex: peersDataIndex, changeHandler: changeHandlers.peerChangedAt)
+            }
+        }
+
+        var removedPeerIndices = [Int]()
+        for (index, peerData) in peersData.enumerated().reversed() {
+            if let peerPublicKey = peerData.publicKey, !other.peers.contains(where: { $0.publicKey == peerPublicKey}) {
+                removedPeerIndices.append(index)
+                peersData.remove(at: index)
+            }
+        }
+        if !removedPeerIndices.isEmpty {
+            changeHandlers.peersRemovedAt(removedPeerIndices)
+        }
+
+        var addedPeerIndices = [Int]()
+        for otherPeer in other.peers {
+            if !peersData.contains(where: { $0.publicKey == otherPeer.publicKey }) {
+                addedPeerIndices.append(peersData.count)
+                let peerData = PeerData(index: peersData.count)
+                peerData.validatedConfiguration = otherPeer
+                peersData.append(peerData)
+            }
+        }
+        if !addedPeerIndices.isEmpty {
+            changeHandlers.peersInsertedAt(addedPeerIndices)
+        }
+
+        for (index, peer) in peersData.enumerated() {
+            peer.index = index
+            peer.numberOfPeers = peersData.count
+            peer.updateExcludePrivateIPsFieldState()
+        }
+    }
 }
 
 extension TunnelViewModel {
index 6a2b1b7eb0d2bd07a2eac3236a244ce20d865140..beb5d24e6c9c929109b50db8aff592e72a9861a5 100644 (file)
@@ -13,12 +13,12 @@ class TunnelDetailTableViewController: UITableViewController {
         case delete
     }
 
-    let interfaceFields: [TunnelViewModel.InterfaceField] = [
+    static let interfaceFields: [TunnelViewModel.InterfaceField] = [
         .name, .publicKey, .addresses,
         .listenPort, .mtu, .dns
     ]
 
-    let peerFields: [TunnelViewModel.PeerField] = [
+    static let peerFields: [TunnelViewModel.PeerField] = [
         .publicKey, .preSharedKey, .endpoint,
         .allowedIPs, .persistentKeepAlive,
         .rxBytes, .txBytes, .lastHandshakeTime
@@ -89,11 +89,11 @@ class TunnelDetailTableViewController: UITableViewController {
     }
 
     private func loadVisibleFields() {
-        let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)
-        interfaceFieldIsVisible = interfaceFields.map { visibleInterfaceFields.contains($0) }
+        let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: TunnelDetailTableViewController.interfaceFields)
+        interfaceFieldIsVisible = TunnelDetailTableViewController.interfaceFields.map { visibleInterfaceFields.contains($0) }
         peerFieldIsVisible = tunnelViewModel.peersData.map { peer in
-            let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: peerFields)
-            return peerFields.map { visiblePeerFields.contains($0) }
+            let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: TunnelDetailTableViewController.peerFields)
+            return TunnelDetailTableViewController.peerFields.map { visiblePeerFields.contains($0) }
         }
     }
 
@@ -172,13 +172,79 @@ class TunnelDetailTableViewController: UITableViewController {
         reloadRuntimeConfigurationTimer = nil
     }
 
+    func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
+        // Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
+        guard let tableView = self.tableView else { return }
+        let sections = self.sections
+        let interfaceSectionIndex = sections.firstIndex(where: { if case .interface = $0 { return true } else { return false }})!
+        let firstPeerSectionIndex = interfaceSectionIndex + 1
+        var interfaceFieldIsVisible = self.interfaceFieldIsVisible
+        var peerFieldIsVisible = self.peerFieldIsVisible
+
+        func sectionChanged<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], tableView: UITableView, section: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) {
+            var fieldIsVisible = fieldIsVisibleInput
+            var modifiedIndexPaths = [IndexPath]()
+            for (index, field) in fields.enumerated() where changes[field] == .modified {
+                let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+                modifiedIndexPaths.append(IndexPath(row: row, section: section))
+            }
+            if !modifiedIndexPaths.isEmpty {
+                tableView.reloadRows(at: modifiedIndexPaths, with: .automatic)
+            }
+
+            var removedIndexPaths = [IndexPath]()
+            for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
+                let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+                removedIndexPaths.append(IndexPath(row: row, section: section))
+                fieldIsVisible[index] = false
+            }
+            if !removedIndexPaths.isEmpty {
+                tableView.deleteRows(at: removedIndexPaths, with: .automatic)
+            }
+
+            var addedIndexPaths = [IndexPath]()
+            for (index, field) in fields.enumerated() where changes[field] == .added {
+                let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+                addedIndexPaths.append(IndexPath(row: row, section: section))
+                fieldIsVisible[index] = true
+            }
+            if !addedIndexPaths.isEmpty {
+                tableView.insertRows(at: addedIndexPaths, with: .automatic)
+            }
+        }
+
+        let changeHandlers = TunnelViewModel.ChangeHandlers(
+            interfaceChanged: { changes in
+                sectionChanged(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
+                               tableView: tableView, section: interfaceSectionIndex, changes: changes)
+            },
+            peerChangedAt: { peerIndex, changes in
+                sectionChanged(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex],
+                               tableView: tableView, section: firstPeerSectionIndex + peerIndex, changes: changes)
+            },
+            peersRemovedAt: { peerIndices in
+                let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
+                tableView.deleteSections(IndexSet(sectionIndices), with: .automatic)
+            },
+            peersInsertedAt: { peerIndices in
+                let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
+                tableView.insertSections(IndexSet(sectionIndices), with: .automatic)
+            }
+        )
+
+        tableView.beginUpdates()
+        self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers)
+        self.loadSections()
+        self.loadVisibleFields()
+        tableView.endUpdates()
+    }
+
     private func reloadRuntimeConfiguration() {
-        tunnel.getRuntimeTunnelConfiguration(completionHandler: {
-            guard let tunnelConfiguration = $0 else { return }
-            self.tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
-            self.loadSections()
-            self.tableView.reloadData()
-        })
+        tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
+            guard let tunnelConfiguration = tunnelConfiguration else { return }
+            guard let self = self else { return }
+            self.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
+        }
     }
 }
 
@@ -261,7 +327,7 @@ extension TunnelDetailTableViewController {
     }
 
     private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
-        let visibleInterfaceFields = interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element }
+        let visibleInterfaceFields = TunnelDetailTableViewController.interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element }
         let field = visibleInterfaceFields[indexPath.row]
         let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
         cell.key = field.localizedUIString
@@ -270,7 +336,7 @@ extension TunnelDetailTableViewController {
     }
 
     private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData, peerIndex: Int) -> UITableViewCell {
-        let visiblePeerFields = peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element }
+        let visiblePeerFields = TunnelDetailTableViewController.peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element }
         let field = visiblePeerFields[indexPath.row]
         let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
         cell.key = field.localizedUIString