aboutsummaryrefslogtreecommitdiffstats
path: root/Sources/WireGuardApp/UI/macOS/View
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/WireGuardApp/UI/macOS/View')
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift71
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift47
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift196
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift89
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift36
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift143
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift66
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift171
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift87
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/highlighter.c641
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/highlighter.h38
11 files changed, 1585 insertions, 0 deletions
diff --git a/Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift b/Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift
new file mode 100644
index 0000000..98e4d49
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ButtonRow: NSView {
+ let button: NSButton = {
+ let button = NSButton()
+ button.title = ""
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ var buttonTitle: String {
+ get { return button.title }
+ set(value) { button.title = value }
+ }
+
+ var isButtonEnabled: Bool {
+ get { return button.isEnabled }
+ set(value) { button.isEnabled = value }
+ }
+
+ var buttonToolTip: String {
+ get { return button.toolTip ?? "" }
+ set(value) { button.toolTip = value }
+ }
+
+ var onButtonClicked: (() -> Void)?
+ var statusObservationToken: AnyObject?
+ var isOnDemandEnabledObservationToken: AnyObject?
+ var hasOnDemandRulesObservationToken: AnyObject?
+
+ override var intrinsicContentSize: NSSize {
+ return NSSize(width: NSView.noIntrinsicMetric, height: button.intrinsicContentSize.height)
+ }
+
+ init() {
+ super.init(frame: CGRect.zero)
+
+ button.target = self
+ button.action = #selector(buttonClicked)
+
+ addSubview(button)
+ button.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 155),
+ button.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
+ ])
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func buttonClicked() {
+ onButtonClicked?()
+ }
+
+ override func prepareForReuse() {
+ buttonTitle = ""
+ buttonToolTip = ""
+ onButtonClicked = nil
+ statusObservationToken = nil
+ isOnDemandEnabledObservationToken = nil
+ hasOnDemandRulesObservationToken = nil
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift b/Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift
new file mode 100644
index 0000000..d08503a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+protocol ConfTextColorTheme {
+ static var defaultColor: NSColor { get }
+ static var colorMap: [UInt32: NSColor] { get }
+}
+
+struct ConfTextAquaColorTheme: ConfTextColorTheme {
+ static let defaultColor = NSColor(hex: "#000000")
+ static let colorMap: [UInt32: NSColor] = [
+ HighlightSection.rawValue: NSColor(hex: "#326D74"), // Class name in Xcode
+ HighlightField.rawValue: NSColor(hex: "#9B2393"), // Keywords in Xcode
+ HighlightPublicKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
+ HighlightPrivateKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
+ HighlightPresharedKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
+ HighlightIP.rawValue: NSColor(hex: "#0E0EFF"), // URLs in Xcode
+ HighlightHost.rawValue: NSColor(hex: "#0E0EFF"), // URLs in Xcode
+ HighlightCidr.rawValue: NSColor(hex: "#815F03"), // Attributes in Xcode
+ HighlightPort.rawValue: NSColor(hex: "#815F03"), // Attributes in Xcode
+ HighlightMTU.rawValue: NSColor(hex: "#1C00CF"), // Numbers in Xcode
+ HighlightKeepalive.rawValue: NSColor(hex: "#1C00CF"), // Numbers in Xcode
+ HighlightComment.rawValue: NSColor(hex: "#536579"), // Comments in Xcode
+ HighlightError.rawValue: NSColor(hex: "#C41A16") // Strings in Xcode
+ ]
+}
+
+struct ConfTextDarkAquaColorTheme: ConfTextColorTheme {
+ static let defaultColor = NSColor(hex: "#FFFFFF") // Plain text in Xcode
+ static let colorMap: [UInt32: NSColor] = [
+ HighlightSection.rawValue: NSColor(hex: "#91D462"), // Class name in Xcode
+ HighlightField.rawValue: NSColor(hex: "#FC5FA3"), // Keywords in Xcode
+ HighlightPublicKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
+ HighlightPrivateKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
+ HighlightPresharedKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
+ HighlightIP.rawValue: NSColor(hex: "#53A5FB"), // URLs in Xcode
+ HighlightHost.rawValue: NSColor(hex: "#53A5FB"), // URLs in Xcode
+ HighlightCidr.rawValue: NSColor(hex: "#75B492"), // Attributes in Xcode
+ HighlightPort.rawValue: NSColor(hex: "#75B492"), // Attributes in Xcode
+ HighlightMTU.rawValue: NSColor(hex: "#9686F5"), // Numbers in Xcode
+ HighlightKeepalive.rawValue: NSColor(hex: "#9686F5"), // Numbers in Xcode
+ HighlightComment.rawValue: NSColor(hex: "#6C7986"), // Comments in Xcode
+ HighlightError.rawValue: NSColor(hex: "#FF4C4C") // Strings in Xcode
+ ]
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift b/Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift
new file mode 100644
index 0000000..574859d
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+private let fontSize: CGFloat = 15
+
+class ConfTextStorage: NSTextStorage {
+ let defaultFont = NSFontManager.shared.convertWeight(true, of: NSFont.systemFont(ofSize: fontSize))
+ private let boldFont = NSFont.boldSystemFont(ofSize: fontSize)
+ private lazy var italicFont = NSFontManager.shared.convert(defaultFont, toHaveTrait: .italicFontMask)
+
+ private var textColorTheme: ConfTextColorTheme.Type?
+
+ private let backingStore: NSMutableAttributedString
+ private(set) var hasError = false
+ private(set) var privateKeyString: String?
+
+ private(set) var hasOnePeer = false
+ private(set) var lastOnePeerAllowedIPs = [String]()
+ private(set) var lastOnePeerDNSServers = [String]()
+ private(set) var lastOnePeerHasPublicKey = false
+
+ override init() {
+ backingStore = NSMutableAttributedString(string: "")
+ super.init()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
+ fatalError("init(pasteboardPropertyList:ofType:) has not been implemented")
+ }
+
+ func nonColorAttributes(for highlightType: highlight_type) -> [NSAttributedString.Key: Any] {
+ switch highlightType.rawValue {
+ case HighlightSection.rawValue, HighlightField.rawValue:
+ return [.font: boldFont]
+ case HighlightPublicKey.rawValue, HighlightPrivateKey.rawValue, HighlightPresharedKey.rawValue,
+ HighlightIP.rawValue, HighlightCidr.rawValue, HighlightHost.rawValue, HighlightPort.rawValue,
+ HighlightMTU.rawValue, HighlightKeepalive.rawValue, HighlightDelimiter.rawValue:
+ return [.font: defaultFont]
+ case HighlightComment.rawValue:
+ return [.font: italicFont]
+ case HighlightError.rawValue:
+ return [.font: defaultFont, .underlineStyle: 1]
+ default:
+ return [:]
+ }
+ }
+
+ func updateAttributes(for textColorTheme: ConfTextColorTheme.Type) {
+ self.textColorTheme = textColorTheme
+ highlightSyntax()
+ }
+
+ override var string: String {
+ return backingStore.string
+ }
+
+ override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
+ return backingStore.attributes(at: location, effectiveRange: range)
+ }
+
+ override func replaceCharacters(in range: NSRange, with str: String) {
+ beginEditing()
+ backingStore.replaceCharacters(in: range, with: str)
+ edited(.editedCharacters, range: range, changeInLength: str.utf16.count - range.length)
+ endEditing()
+ }
+
+ override func replaceCharacters(in range: NSRange, with attrString: NSAttributedString) {
+ beginEditing()
+ backingStore.replaceCharacters(in: range, with: attrString)
+ edited(.editedCharacters, range: range, changeInLength: attrString.length - range.length)
+ endEditing()
+ }
+
+ override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
+ beginEditing()
+ backingStore.setAttributes(attrs, range: range)
+ edited(.editedAttributes, range: range, changeInLength: 0)
+ endEditing()
+ }
+
+ func resetLastPeer() {
+ hasOnePeer = false
+ lastOnePeerAllowedIPs = []
+ lastOnePeerDNSServers = []
+ lastOnePeerHasPublicKey = false
+ }
+
+ func evaluateExcludePrivateIPs(highlightSpans: UnsafePointer<highlight_span>) {
+ var spans = highlightSpans
+ let string = backingStore.string
+ enum FieldType: String {
+ case dns
+ case allowedips
+ }
+ var fieldType: FieldType?
+ resetLastPeer()
+ while spans.pointee.type != HighlightEnd {
+ let span = spans.pointee
+ var substring = String(string.substring(higlightSpan: span)).lowercased()
+
+ if span.type == HighlightError {
+ resetLastPeer()
+ return
+ } else if span.type == HighlightSection {
+ if substring == "[peer]" {
+ if hasOnePeer {
+ resetLastPeer()
+ return
+ }
+ hasOnePeer = true
+ }
+ } else if span.type == HighlightField {
+ fieldType = FieldType(rawValue: substring)
+ } else if span.type == HighlightIP && fieldType == .dns {
+ lastOnePeerDNSServers.append(substring)
+ } else if span.type == HighlightIP && fieldType == .allowedips {
+ let next = spans.successor()
+ let nextnext = next.successor()
+ if next.pointee.type == HighlightDelimiter && nextnext.pointee.type == HighlightCidr {
+ let delimiter = string.substring(higlightSpan: next.pointee)
+ let cidr = string.substring(higlightSpan: nextnext.pointee)
+ substring += delimiter + cidr
+ }
+ lastOnePeerAllowedIPs.append(substring)
+ } else if span.type == HighlightPublicKey {
+ lastOnePeerHasPublicKey = true
+ }
+ spans = spans.successor()
+ }
+ }
+
+ func highlightSyntax() {
+ guard let textColorTheme = textColorTheme else { return }
+ hasError = false
+ privateKeyString = nil
+
+ let string = backingStore.string
+ let fullTextRange = NSRange(..<string.endIndex, in: string)
+
+ backingStore.beginEditing()
+ let defaultAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: textColorTheme.defaultColor,
+ .font: defaultFont
+ ]
+ backingStore.setAttributes(defaultAttributes, range: fullTextRange)
+ var spans = highlight_config(string)!
+ evaluateExcludePrivateIPs(highlightSpans: spans)
+
+ let spansStart = spans
+ while spans.pointee.type != HighlightEnd {
+ let span = spans.pointee
+
+ let startIndex = string.utf8.index(string.startIndex, offsetBy: span.start)
+ let endIndex = string.utf8.index(startIndex, offsetBy: span.len)
+ let range = NSRange(startIndex..<endIndex, in: string)
+
+ backingStore.setAttributes(nonColorAttributes(for: span.type), range: range)
+
+ let color = textColorTheme.colorMap[span.type.rawValue, default: textColorTheme.defaultColor]
+ backingStore.addAttribute(.foregroundColor, value: color, range: range)
+
+ if span.type == HighlightError {
+ hasError = true
+ }
+
+ if span.type == HighlightPrivateKey {
+ privateKeyString = String(string.substring(higlightSpan: span))
+ }
+
+ spans = spans.successor()
+ }
+ backingStore.endEditing()
+ free(spansStart)
+
+ beginEditing()
+ edited(.editedAttributes, range: fullTextRange, changeInLength: 0)
+ endEditing()
+ }
+
+}
+
+private extension String {
+ func substring(higlightSpan span: highlight_span) -> Substring {
+ let startIndex = self.utf8.index(self.utf8.startIndex, offsetBy: span.start)
+ let endIndex = self.utf8.index(startIndex, offsetBy: span.len)
+
+ return self[startIndex..<endIndex]
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift b/Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift
new file mode 100644
index 0000000..eafaf12
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ConfTextView: NSTextView {
+
+ private let confTextStorage = ConfTextStorage()
+
+ @objc dynamic var hasError = false
+ @objc dynamic var privateKeyString: String?
+ @objc dynamic var singlePeerAllowedIPs: [String]?
+
+ override var string: String {
+ didSet {
+ confTextStorage.highlightSyntax()
+ updateConfigData()
+ }
+ }
+
+ init() {
+ let textContainer = NSTextContainer()
+ let layoutManager = NSLayoutManager()
+ layoutManager.addTextContainer(textContainer)
+ confTextStorage.addLayoutManager(layoutManager)
+ super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 60), textContainer: textContainer)
+ font = confTextStorage.defaultFont
+ allowsUndo = true
+ isAutomaticSpellingCorrectionEnabled = false
+ isAutomaticDataDetectionEnabled = false
+ isAutomaticLinkDetectionEnabled = false
+ isAutomaticTextCompletionEnabled = false
+ isAutomaticTextReplacementEnabled = false
+ isAutomaticDashSubstitutionEnabled = false
+ isAutomaticQuoteSubstitutionEnabled = false
+ updateTheme()
+ delegate = self
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidChangeEffectiveAppearance() {
+ updateTheme()
+ }
+
+ private func updateTheme() {
+ switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) ?? .aqua {
+ case .darkAqua:
+ confTextStorage.updateAttributes(for: ConfTextDarkAquaColorTheme.self)
+ default:
+ confTextStorage.updateAttributes(for: ConfTextAquaColorTheme.self)
+ }
+ }
+
+ private func updateConfigData() {
+ if hasError != confTextStorage.hasError {
+ hasError = confTextStorage.hasError
+ }
+ if privateKeyString != confTextStorage.privateKeyString {
+ privateKeyString = confTextStorage.privateKeyString
+ }
+ let hasSyntaxError = confTextStorage.hasError
+ let hasSemanticError = confTextStorage.privateKeyString == nil || !confTextStorage.lastOnePeerHasPublicKey
+ let updatedSinglePeerAllowedIPs = confTextStorage.hasOnePeer && !hasSyntaxError && !hasSemanticError ? confTextStorage.lastOnePeerAllowedIPs : nil
+ if singlePeerAllowedIPs != updatedSinglePeerAllowedIPs {
+ singlePeerAllowedIPs = updatedSinglePeerAllowedIPs
+ }
+ }
+
+ func setConfText(_ text: String) {
+ let fullTextRange = NSRange(..<string.endIndex, in: string)
+ if shouldChangeText(in: fullTextRange, replacementString: text) {
+ replaceCharacters(in: fullTextRange, with: text)
+ didChangeText()
+ }
+ }
+}
+
+extension ConfTextView: NSTextViewDelegate {
+
+ func textDidChange(_ notification: Notification) {
+ confTextStorage.highlightSyntax()
+ updateConfigData()
+ needsDisplay = true
+ }
+
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift b/Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift
new file mode 100644
index 0000000..59cc556
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class DeleteTunnelsConfirmationAlert: NSAlert {
+ var alertDeleteButton: NSButton?
+ var alertCancelButton: NSButton?
+
+ var onDeleteClicked: ((_ completionHandler: @escaping () -> Void) -> Void)?
+
+ override init() {
+ super.init()
+ let alertDeleteButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleDelete"))
+ alertDeleteButton.target = self
+ alertDeleteButton.action = #selector(removeTunnelAlertDeleteClicked)
+ self.alertDeleteButton = alertDeleteButton
+ self.alertCancelButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleCancel"))
+ }
+
+ @objc func removeTunnelAlertDeleteClicked() {
+ alertDeleteButton?.title = tr("macDeleteTunnelConfirmationAlertButtonTitleDeleting")
+ alertDeleteButton?.isEnabled = false
+ alertCancelButton?.isEnabled = false
+ if let onDeleteClicked = onDeleteClicked {
+ onDeleteClicked { [weak self] in
+ guard let self = self else { return }
+ self.window.sheetParent?.endSheet(self.window)
+ }
+ }
+ }
+
+ func beginSheetModal(for sheetWindow: NSWindow) {
+ beginSheetModal(for: sheetWindow) { _ in }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift b/Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift
new file mode 100644
index 0000000..2ce31a6
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class EditableKeyValueRow: NSView {
+ let keyLabel: NSTextField = {
+ let keyLabel = NSTextField()
+ keyLabel.isEditable = false
+ keyLabel.isSelectable = false
+ keyLabel.isBordered = false
+ keyLabel.alignment = .right
+ keyLabel.maximumNumberOfLines = 1
+ keyLabel.lineBreakMode = .byTruncatingTail
+ keyLabel.backgroundColor = .clear
+ return keyLabel
+ }()
+
+ let valueLabel: NSTextField = {
+ let valueLabel = NSTextField()
+ valueLabel.isSelectable = true
+ valueLabel.maximumNumberOfLines = 1
+ valueLabel.lineBreakMode = .byTruncatingTail
+ return valueLabel
+ }()
+
+ let valueImageView: NSImageView?
+
+ var key: String {
+ get { return keyLabel.stringValue }
+ set(value) { keyLabel.stringValue = value }
+ }
+ var value: String {
+ get { return valueLabel.stringValue }
+ set(value) { valueLabel.stringValue = value }
+ }
+ var isKeyInBold: Bool {
+ get { return keyLabel.font == NSFont.boldSystemFont(ofSize: 0) }
+ set(value) {
+ if value {
+ keyLabel.font = NSFont.boldSystemFont(ofSize: 0)
+ } else {
+ keyLabel.font = NSFont.systemFont(ofSize: 0)
+ }
+ }
+ }
+ var valueImage: NSImage? {
+ get { return valueImageView?.image }
+ set(value) { valueImageView?.image = value }
+ }
+
+ var statusObservationToken: AnyObject?
+ var isOnDemandEnabledObservationToken: AnyObject?
+ var hasOnDemandRulesObservationToken: AnyObject?
+
+ override var intrinsicContentSize: NSSize {
+ let height = max(keyLabel.intrinsicContentSize.height, valueLabel.intrinsicContentSize.height)
+ return NSSize(width: NSView.noIntrinsicMetric, height: height)
+ }
+
+ convenience init() {
+ self.init(hasValueImage: false)
+ }
+
+ fileprivate init(hasValueImage: Bool) {
+ valueImageView = hasValueImage ? NSImageView() : nil
+ super.init(frame: CGRect.zero)
+
+ addSubview(keyLabel)
+ addSubview(valueLabel)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ valueLabel.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ keyLabel.firstBaselineAnchor.constraint(equalTo: valueLabel.firstBaselineAnchor),
+ self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
+ valueLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)
+ ])
+
+ let spacing: CGFloat = 5
+ if let valueImageView = valueImageView {
+ addSubview(valueImageView)
+ valueImageView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ valueImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ valueImageView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing),
+ valueLabel.leadingAnchor.constraint(equalTo: valueImageView.trailingAnchor)
+ ])
+ } else {
+ NSLayoutConstraint.activate([
+ valueLabel.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing)
+ ])
+ }
+
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
+ keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+ let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
+ widthConstraint.priority = .defaultHigh + 1
+ widthConstraint.isActive = true
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ key = ""
+ value = ""
+ isKeyInBold = false
+ statusObservationToken = nil
+ isOnDemandEnabledObservationToken = nil
+ hasOnDemandRulesObservationToken = nil
+ }
+}
+
+class KeyValueRow: EditableKeyValueRow {
+ init() {
+ super.init(hasValueImage: false)
+ valueLabel.isEditable = false
+ valueLabel.isBordered = false
+ valueLabel.backgroundColor = .clear
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
+
+class KeyValueImageRow: EditableKeyValueRow {
+ init() {
+ super.init(hasValueImage: true)
+ valueLabel.isEditable = false
+ valueLabel.isBordered = false
+ valueLabel.backgroundColor = .clear
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift b/Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift
new file mode 100644
index 0000000..50eb2bd
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class LogViewCell: NSTableCellView {
+ var text: String = "" {
+ didSet { textField?.stringValue = text }
+ }
+
+ init() {
+ super.init(frame: .zero)
+
+ let textField = NSTextField(wrappingLabelWithString: "")
+ addSubview(textField)
+ textField.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ textField.leadingAnchor.constraint(equalTo: self.leadingAnchor),
+ textField.trailingAnchor.constraint(equalTo: self.trailingAnchor),
+ textField.topAnchor.constraint(equalTo: self.topAnchor),
+ textField.bottomAnchor.constraint(equalTo: self.bottomAnchor)
+ ])
+
+ self.textField = textField
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ textField?.stringValue = ""
+ }
+}
+
+class LogViewTimestampCell: LogViewCell {
+ override init() {
+ super.init()
+ if let textField = textField {
+ textField.maximumNumberOfLines = 1
+ textField.lineBreakMode = .byClipping
+ textField.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
+ textField.setContentHuggingPriority(.defaultLow, for: .vertical)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
+
+class LogViewMessageCell: LogViewCell {
+ override init() {
+ super.init()
+ if let textField = textField {
+ textField.maximumNumberOfLines = 0
+ textField.lineBreakMode = .byWordWrapping
+ textField.setContentCompressionResistancePriority(.required, for: .vertical)
+ textField.setContentHuggingPriority(.required, for: .vertical)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift b/Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift
new file mode 100644
index 0000000..fb9ff35
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+import CoreWLAN
+
+class OnDemandControlsRow: NSView {
+ let keyLabel: NSTextField = {
+ let keyLabel = NSTextField()
+ keyLabel.stringValue = tr("macFieldOnDemand")
+ keyLabel.isEditable = false
+ keyLabel.isSelectable = false
+ keyLabel.isBordered = false
+ keyLabel.alignment = .right
+ keyLabel.maximumNumberOfLines = 1
+ keyLabel.lineBreakMode = .byTruncatingTail
+ keyLabel.backgroundColor = .clear
+ return keyLabel
+ }()
+
+ let onDemandEthernetCheckbox: NSButton = {
+ let checkbox = NSButton()
+ checkbox.title = tr("tunnelOnDemandEthernet")
+ checkbox.setButtonType(.switch)
+ checkbox.state = .off
+ return checkbox
+ }()
+
+ let onDemandWiFiCheckbox: NSButton = {
+ let checkbox = NSButton()
+ checkbox.title = tr("tunnelOnDemandWiFi")
+ checkbox.setButtonType(.switch)
+ checkbox.state = .off
+ return checkbox
+ }()
+
+ static let onDemandSSIDOptions: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
+ .anySSID, .onlySpecificSSIDs, .exceptSpecificSSIDs
+ ]
+
+ let onDemandSSIDOptionsPopup = NSPopUpButton()
+
+ let onDemandSSIDsField: NSTokenField = {
+ let tokenField = NSTokenField()
+ tokenField.tokenizingCharacterSet = CharacterSet([])
+ tokenField.tokenStyle = .squared
+ NSLayoutConstraint.activate([
+ tokenField.widthAnchor.constraint(greaterThanOrEqualToConstant: 180)
+ ])
+ return tokenField
+ }()
+
+ override var intrinsicContentSize: NSSize {
+ let minHeight: CGFloat = 22
+ let height = max(minHeight, keyLabel.intrinsicContentSize.height,
+ onDemandEthernetCheckbox.intrinsicContentSize.height, onDemandWiFiCheckbox.intrinsicContentSize.height,
+ onDemandSSIDOptionsPopup.intrinsicContentSize.height, onDemandSSIDsField.intrinsicContentSize.height)
+ return NSSize(width: NSView.noIntrinsicMetric, height: height)
+ }
+
+ var onDemandViewModel: ActivateOnDemandViewModel? {
+ didSet { updateControls() }
+ }
+
+ var currentSSIDs: [String]
+
+ init() {
+ currentSSIDs = getCurrentSSIDs()
+ super.init(frame: CGRect.zero)
+
+ onDemandSSIDOptionsPopup.addItems(withTitles: OnDemandControlsRow.onDemandSSIDOptions.map { $0.localizedUIString })
+
+ let stackView = NSStackView()
+ stackView.setViews([onDemandEthernetCheckbox, onDemandWiFiCheckbox, onDemandSSIDOptionsPopup, onDemandSSIDsField], in: .leading)
+ stackView.orientation = .horizontal
+
+ addSubview(keyLabel)
+ addSubview(stackView)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
+ stackView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: 5),
+ stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
+ ])
+
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
+ keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+
+ let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
+ widthConstraint.priority = .defaultHigh + 1
+ widthConstraint.isActive = true
+
+ NSLayoutConstraint.activate([
+ onDemandEthernetCheckbox.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),
+ onDemandWiFiCheckbox.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
+ onDemandSSIDOptionsPopup.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
+ onDemandSSIDsField.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor)
+ ])
+
+ onDemandSSIDsField.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+ onDemandEthernetCheckbox.target = self
+ onDemandEthernetCheckbox.action = #selector(ethernetCheckboxToggled)
+
+ onDemandWiFiCheckbox.target = self
+ onDemandWiFiCheckbox.action = #selector(wiFiCheckboxToggled)
+
+ onDemandSSIDOptionsPopup.target = self
+ onDemandSSIDOptionsPopup.action = #selector(ssidOptionsPopupValueChanged)
+
+ onDemandSSIDsField.delegate = self
+
+ updateControls()
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func saveToViewModel() {
+ guard let onDemandViewModel = onDemandViewModel else { return }
+ onDemandViewModel.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
+ onDemandViewModel.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
+ onDemandViewModel.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[onDemandSSIDOptionsPopup.indexOfSelectedItem]
+ onDemandViewModel.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
+ }
+
+ func updateControls() {
+ guard let onDemandViewModel = onDemandViewModel else { return }
+ onDemandEthernetCheckbox.state = onDemandViewModel.isNonWiFiInterfaceEnabled ? .on : .off
+ onDemandWiFiCheckbox.state = onDemandViewModel.isWiFiInterfaceEnabled ? .on : .off
+ let optionIndex = OnDemandControlsRow.onDemandSSIDOptions.firstIndex(of: onDemandViewModel.ssidOption)
+ onDemandSSIDOptionsPopup.selectItem(at: optionIndex ?? 0)
+ onDemandSSIDsField.objectValue = onDemandViewModel.selectedSSIDs
+ onDemandSSIDOptionsPopup.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled
+ onDemandSSIDsField.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled || onDemandViewModel.ssidOption == .anySSID
+ }
+
+ @objc func ethernetCheckboxToggled() {
+ onDemandViewModel?.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
+ }
+
+ @objc func wiFiCheckboxToggled() {
+ onDemandViewModel?.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
+ updateControls()
+ }
+
+ @objc func ssidOptionsPopupValueChanged() {
+ let selectedIndex = onDemandSSIDOptionsPopup.indexOfSelectedItem
+ onDemandViewModel?.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[selectedIndex]
+ onDemandViewModel?.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
+ updateControls()
+ if !onDemandSSIDsField.isHidden {
+ onDemandSSIDsField.becomeFirstResponder()
+ }
+ }
+}
+
+extension OnDemandControlsRow: NSTokenFieldDelegate {
+ func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
+ return currentSSIDs.filter { $0.hasPrefix(substring) }
+ }
+}
+
+private func getCurrentSSIDs() -> [String] {
+ return CWWiFiClient.shared().interfaces()?.compactMap { $0.ssid() } ?? []
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift b/Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift
new file mode 100644
index 0000000..9f844b1
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class TunnelListRow: NSView {
+ var tunnel: TunnelContainer? {
+ didSet(value) {
+ // Bind to the tunnel's name
+ nameLabel.stringValue = tunnel?.name ?? ""
+ nameObservationToken = tunnel?.observe(\TunnelContainer.name) { [weak self] tunnel, _ in
+ self?.nameLabel.stringValue = tunnel.name
+ }
+ // Bind to the tunnel's status
+ statusImageView.image = TunnelListRow.image(for: tunnel)
+ statusObservationToken = tunnel?.observe(\TunnelContainer.status) { [weak self] tunnel, _ in
+ self?.statusImageView.image = TunnelListRow.image(for: tunnel)
+ }
+ isOnDemandEnabledObservationToken = tunnel?.observe(\TunnelContainer.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
+ self?.statusImageView.image = TunnelListRow.image(for: tunnel)
+ }
+ }
+ }
+
+ let nameLabel: NSTextField = {
+ let nameLabel = NSTextField()
+ nameLabel.isEditable = false
+ nameLabel.isSelectable = false
+ nameLabel.isBordered = false
+ nameLabel.maximumNumberOfLines = 1
+ nameLabel.lineBreakMode = .byTruncatingTail
+ return nameLabel
+ }()
+
+ let statusImageView = NSImageView()
+
+ private var statusObservationToken: AnyObject?
+ private var nameObservationToken: AnyObject?
+ private var isOnDemandEnabledObservationToken: AnyObject?
+
+ init() {
+ super.init(frame: CGRect.zero)
+
+ addSubview(statusImageView)
+ addSubview(nameLabel)
+ statusImageView.translatesAutoresizingMaskIntoConstraints = false
+ nameLabel.translatesAutoresizingMaskIntoConstraints = false
+ nameLabel.backgroundColor = .clear
+ NSLayoutConstraint.activate([
+ self.leadingAnchor.constraint(equalTo: statusImageView.leadingAnchor),
+ statusImageView.trailingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
+ statusImageView.widthAnchor.constraint(equalToConstant: 20),
+ nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
+ statusImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor)
+ ])
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ static func image(for tunnel: TunnelContainer?) -> NSImage? {
+ guard let tunnel = tunnel else { return nil }
+ switch tunnel.status {
+ case .active, .restarting, .reasserting:
+ return NSImage(named: NSImage.statusAvailableName)
+ case .activating, .waiting, .deactivating:
+ return NSImage(named: NSImage.statusPartiallyAvailableName)
+ case .inactive:
+ if tunnel.isActivateOnDemandEnabled {
+ return NSImage(named: NSImage.Name.statusOnDemandEnabled)
+ } else {
+ return NSImage(named: NSImage.statusNoneName)
+ }
+ }
+ }
+
+ override func prepareForReuse() {
+ nameLabel.stringValue = ""
+ statusImageView.image = nil
+ }
+}
+
+extension NSImage.Name {
+ static let statusOnDemandEnabled = NSImage.Name("StatusCircleYellow")
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/highlighter.c b/Sources/WireGuardApp/UI/macOS/View/highlighter.c
new file mode 100644
index 0000000..625b7e0
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/highlighter.c
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "highlighter.h"
+
+typedef struct {
+ const char *s;
+ size_t len;
+} string_span_t;
+
+static bool is_decimal(char c)
+{
+ return c >= '0' && c <= '9';
+}
+
+static bool is_hexadecimal(char c)
+{
+ return is_decimal(c) || ((c | 32) >= 'a' && (c | 32) <= 'f');
+}
+
+static bool is_alphabet(char c)
+{
+ return (c | 32) >= 'a' && (c | 32) <= 'z';
+}
+
+static bool is_same(string_span_t s, const char *c)
+{
+ size_t len = strlen(c);
+
+ if (len != s.len)
+ return false;
+ return !memcmp(s.s, c, len);
+}
+
+static bool is_caseless_same(string_span_t s, const char *c)
+{
+ size_t len = strlen(c);
+
+ if (len != s.len)
+ return false;
+ for (size_t i = 0; i < len; ++i) {
+ char a = c[i], b = s.s[i];
+ if ((unsigned)a - 'a' < 26)
+ a &= 95;
+ if ((unsigned)b - 'a' < 26)
+ b &= 95;
+ if (a != b)
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_key(string_span_t s)
+{
+ if (s.len != 44 || s.s[43] != '=')
+ return false;
+
+ for (size_t i = 0; i < 42; ++i) {
+ if (!is_decimal(s.s[i]) && !is_alphabet(s.s[i]) &&
+ s.s[i] != '/' && s.s[i] != '+')
+ return false;
+ }
+ switch (s.s[42]) {
+ case 'A':
+ case 'E':
+ case 'I':
+ case 'M':
+ case 'Q':
+ case 'U':
+ case 'Y':
+ case 'c':
+ case 'g':
+ case 'k':
+ case 'o':
+ case 's':
+ case 'w':
+ case '4':
+ case '8':
+ case '0':
+ break;
+ default:
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_hostname(string_span_t s)
+{
+ size_t num_digit = 0, num_entity = s.len;
+
+ if (s.len > 63 || !s.len)
+ return false;
+ if (s.s[0] == '-' || s.s[s.len - 1] == '-')
+ return false;
+ if (s.s[0] == '.' || s.s[s.len - 1] == '.')
+ return false;
+
+ for (size_t i = 0; i < s.len; ++i) {
+ if (is_decimal(s.s[i])) {
+ ++num_digit;
+ continue;
+ }
+ if (s.s[i] == '.') {
+ --num_entity;
+ continue;
+ }
+
+ if (!is_alphabet(s.s[i]) && s.s[i] != '-')
+ return false;
+
+ if (i && s.s[i] == '.' && s.s[i - 1] == '.')
+ return false;
+ }
+ return num_digit != num_entity;
+}
+
+static bool is_valid_ipv4(string_span_t s)
+{
+ for (size_t j, i = 0, pos = 0; i < 4 && pos < s.len; ++i) {
+ uint32_t val = 0;
+
+ for (j = 0; j < 3 && pos + j < s.len && is_decimal(s.s[pos + j]); ++j)
+ val = 10 * val + s.s[pos + j] - '0';
+ if (j == 0 || (j > 1 && s.s[pos] == '0') || val > 255)
+ return false;
+ if (pos + j == s.len && i == 3)
+ return true;
+ if (s.s[pos + j] != '.')
+ return false;
+ pos += j + 1;
+ }
+ return false;
+}
+
+static bool is_valid_ipv6(string_span_t s)
+{
+ size_t pos = 0;
+ bool seen_colon = false;
+
+ if (s.len < 2)
+ return false;
+ if (s.s[pos] == ':' && s.s[++pos] != ':')
+ return false;
+ if (s.s[s.len - 1] == ':' && s.s[s.len - 2] != ':')
+ return false;
+
+ for (size_t j, i = 0; pos < s.len; ++i) {
+ if (s.s[pos] == ':' && !seen_colon) {
+ seen_colon = true;
+ if (++pos == s.len)
+ break;
+ if (i == 7)
+ return false;
+ continue;
+ }
+ for (j = 0; j < 4 && pos + j < s.len && is_hexadecimal(s.s[pos + j]); ++j);
+ if (j == 0)
+ return false;
+ if (pos + j == s.len && (seen_colon || i == 7))
+ break;
+ if (i == 7)
+ return false;
+ if (s.s[pos + j] != ':') {
+ if (s.s[pos + j] != '.' || (i < 6 && !seen_colon))
+ return false;
+ return is_valid_ipv4((string_span_t){ s.s + pos, s.len - pos });
+ }
+ pos += j + 1;
+ }
+ return true;
+}
+
+static bool is_valid_uint(string_span_t s, bool support_hex, uint64_t min, uint64_t max)
+{
+ uint64_t val = 0;
+
+ /* Bound this around 32 bits, so that we don't have to write overflow logic. */
+ if (s.len > 10 || !s.len)
+ return false;
+
+ if (support_hex && s.len > 2 && s.s[0] == '0' && s.s[1] == 'x') {
+ for (size_t i = 2; i < s.len; ++i) {
+ if ((unsigned)s.s[i] - '0' < 10)
+ val = 16 * val + (s.s[i] - '0');
+ else if (((unsigned)s.s[i] | 32) - 'a' < 6)
+ val = 16 * val + (s.s[i] | 32) - 'a' + 10;
+ else
+ return false;
+ }
+ } else {
+ for (size_t i = 0; i < s.len; ++i) {
+ if (!is_decimal(s.s[i]))
+ return false;
+ val = 10 * val + s.s[i] - '0';
+ }
+ }
+ return val <= max && val >= min;
+}
+
+static bool is_valid_port(string_span_t s)
+{
+ return is_valid_uint(s, false, 0, 65535);
+}
+
+static bool is_valid_mtu(string_span_t s)
+{
+ return is_valid_uint(s, false, 576, 65535);
+}
+
+static bool is_valid_persistentkeepalive(string_span_t s)
+{
+ if (is_same(s, "off"))
+ return true;
+ return is_valid_uint(s, false, 0, 65535);
+}
+
+#ifndef MOBILE_WGQUICK_SUBSET
+
+static bool is_valid_fwmark(string_span_t s)
+{
+ if (is_same(s, "off"))
+ return true;
+ return is_valid_uint(s, true, 0, 4294967295);
+}
+
+static bool is_valid_table(string_span_t s)
+{
+ if (is_same(s, "auto"))
+ return true;
+ if (is_same(s, "off"))
+ return true;
+ /* This pretty much invalidates the other checks, but rt_names.c's
+ * fread_id_name does no validation aside from this. */
+ if (s.len < 512)
+ return true;
+ return is_valid_uint(s, false, 0, 4294967295);
+}
+
+static bool is_valid_saveconfig(string_span_t s)
+{
+ return is_same(s, "true") || is_same(s, "false");
+}
+
+static bool is_valid_prepostupdown(string_span_t s)
+{
+ /* It's probably not worthwhile to try to validate a bash expression.
+ * So instead we just demand non-zero length. */
+ return s.len;
+}
+#endif
+
+static bool is_valid_scope(string_span_t s)
+{
+ if (s.len > 64 || !s.len)
+ return false;
+ for (size_t i = 0; i < s.len; ++i) {
+ if (!is_alphabet(s.s[i]) && !is_decimal(s.s[i]) &&
+ s.s[i] != '_' && s.s[i] != '=' && s.s[i] != '+' &&
+ s.s[i] != '.' && s.s[i] != '-')
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_endpoint(string_span_t s)
+{
+
+ if (!s.len)
+ return false;
+
+ if (s.s[0] == '[') {
+ bool seen_scope = false;
+ string_span_t hostspan = { s.s + 1, 0 };
+
+ for (size_t i = 1; i < s.len; ++i) {
+ if (s.s[i] == '%') {
+ if (seen_scope)
+ return false;
+ seen_scope = true;
+ if (!is_valid_ipv6(hostspan))
+ return false;
+ hostspan = (string_span_t){ s.s + i + 1, 0 };
+ } else if (s.s[i] == ']') {
+ if (seen_scope) {
+ if (!is_valid_scope(hostspan))
+ return false;
+ } else if (!is_valid_ipv6(hostspan)) {
+ return false;
+ }
+ if (i == s.len - 1 || s.s[i + 1] != ':')
+ return false;
+ return is_valid_port((string_span_t){ s.s + i + 2, s.len - i - 2 });
+ } else {
+ ++hostspan.len;
+ }
+ }
+ return false;
+ }
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == ':') {
+ string_span_t host = { s.s, i }, port = { s.s + i + 1, s.len - i - 1};
+ return is_valid_port(port) && (is_valid_ipv4(host) || is_valid_hostname(host));
+ }
+ }
+ return false;
+}
+
+static bool is_valid_network(string_span_t s)
+{
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == '/') {
+ string_span_t ip = { s.s, i }, cidr = { s.s + i + 1, s.len - i - 1};
+ uint16_t cidrval = 0;
+
+ if (cidr.len > 3 || !cidr.len)
+ return false;
+
+ for (size_t j = 0; j < cidr.len; ++j) {
+ if (!is_decimal(cidr.s[j]))
+ return false;
+ cidrval = 10 * cidrval + cidr.s[j] - '0';
+ }
+ if (is_valid_ipv4(ip))
+ return cidrval <= 32;
+ else if (is_valid_ipv6(ip))
+ return cidrval <= 128;
+ return false;
+ }
+ }
+ return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+enum field {
+ InterfaceSection,
+ PrivateKey,
+ ListenPort,
+ Address,
+ DNS,
+ MTU,
+#ifndef MOBILE_WGQUICK_SUBSET
+ FwMark,
+ Table,
+ PreUp, PostUp, PreDown, PostDown,
+ SaveConfig,
+#endif
+
+ PeerSection,
+ PublicKey,
+ PresharedKey,
+ AllowedIPs,
+ Endpoint,
+ PersistentKeepalive,
+
+ Invalid
+};
+
+static enum field section_for_field(enum field t)
+{
+ if (t > InterfaceSection && t < PeerSection)
+ return InterfaceSection;
+ if (t > PeerSection && t < Invalid)
+ return PeerSection;
+ return Invalid;
+}
+
+static enum field get_field(string_span_t s)
+{
+#define check_enum(t) do { if (is_caseless_same(s, #t)) return t; } while (0)
+ check_enum(PrivateKey);
+ check_enum(ListenPort);
+ check_enum(Address);
+ check_enum(DNS);
+ check_enum(MTU);
+ check_enum(PublicKey);
+ check_enum(PresharedKey);
+ check_enum(AllowedIPs);
+ check_enum(Endpoint);
+ check_enum(PersistentKeepalive);
+#ifndef MOBILE_WGQUICK_SUBSET
+ check_enum(FwMark);
+ check_enum(Table);
+ check_enum(PreUp);
+ check_enum(PostUp);
+ check_enum(PreDown);
+ check_enum(PostDown);
+ check_enum(SaveConfig);
+#endif
+ return Invalid;
+#undef check_enum
+}
+
+static enum field get_sectiontype(string_span_t s)
+{
+ if (is_caseless_same(s, "[Peer]"))
+ return PeerSection;
+ if (is_caseless_same(s, "[Interface]"))
+ return InterfaceSection;
+ return Invalid;
+}
+
+struct highlight_span_array {
+ size_t len, capacity;
+ struct highlight_span *spans;
+};
+
+/* A useful OpenBSD-ism. */
+static void *realloc_array(void *optr, size_t nmemb, size_t size)
+{
+ if ((nmemb >= (size_t)1 << (sizeof(size_t) * 4) ||
+ size >= (size_t)1 << (sizeof(size_t) * 4)) &&
+ nmemb > 0 && SIZE_MAX / nmemb < size) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ return realloc(optr, size * nmemb);
+}
+
+static bool append_highlight_span(struct highlight_span_array *a, const char *o, string_span_t s, enum highlight_type t)
+{
+ if (!s.len)
+ return true;
+ if (a->len >= a->capacity) {
+ struct highlight_span *resized;
+
+ a->capacity = a->capacity ? a->capacity * 2 : 64;
+ resized = realloc_array(a->spans, a->capacity, sizeof(*resized));
+ if (!resized) {
+ free(a->spans);
+ memset(a, 0, sizeof(*a));
+ return false;
+ }
+ a->spans = resized;
+ }
+ a->spans[a->len++] = (struct highlight_span){ t, s.s - o, s.len };
+ return true;
+}
+
+static void highlight_multivalue_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ switch (section) {
+ case DNS:
+ if (is_valid_ipv4(s) || is_valid_ipv6(s))
+ append_highlight_span(ret, parent.s, s, HighlightIP);
+ else if (is_valid_hostname(s))
+ append_highlight_span(ret, parent.s, s, HighlightHost);
+ else
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ break;
+ case Address:
+ case AllowedIPs: {
+ size_t slash;
+
+ if (!is_valid_network(s)) {
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ break;
+ }
+ for (slash = 0; slash < s.len; ++slash) {
+ if (s.s[slash] == '/')
+ break;
+ }
+ if (slash == s.len) {
+ append_highlight_span(ret, parent.s, s, HighlightIP);
+ } else {
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s, slash }, HighlightIP);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash, 1 }, HighlightDelimiter);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash + 1, s.len - slash - 1 }, HighlightCidr);
+ }
+ break;
+ }
+ default:
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ }
+}
+
+static void highlight_multivalue(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ string_span_t current_span = { s.s, 0 };
+ size_t len_at_last_space = 0;
+
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == ',') {
+ current_span.len = len_at_last_space;
+ highlight_multivalue_value(ret, parent, current_span, section);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + i, 1 }, HighlightDelimiter);
+ len_at_last_space = 0;
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+ if (&s.s[i] == current_span.s && !current_span.len)
+ ++current_span.s;
+ else
+ ++current_span.len;
+ } else {
+ len_at_last_space = ++current_span.len;
+ }
+ }
+ current_span.len = len_at_last_space;
+ if (current_span.len)
+ highlight_multivalue_value(ret, parent, current_span, section);
+ else if (ret->spans[ret->len - 1].type == HighlightDelimiter)
+ ret->spans[ret->len - 1].type = HighlightError;
+}
+
+static void highlight_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ switch (section) {
+ case PrivateKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPrivateKey : HighlightError);
+ break;
+ case PublicKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPublicKey : HighlightError);
+ break;
+ case PresharedKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPresharedKey : HighlightError);
+ break;
+ case MTU:
+ append_highlight_span(ret, parent.s, s, is_valid_mtu(s) ? HighlightMTU : HighlightError);
+ break;
+#ifndef MOBILE_WGQUICK_SUBSET
+ case SaveConfig:
+ append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? HighlightSaveConfig : HighlightError);
+ break;
+ case FwMark:
+ append_highlight_span(ret, parent.s, s, is_valid_fwmark(s) ? HighlightFwMark : HighlightError);
+ break;
+ case Table:
+ append_highlight_span(ret, parent.s, s, is_valid_table(s) ? HighlightTable : HighlightError);
+ break;
+ case PreUp:
+ case PostUp:
+ case PreDown:
+ case PostDown:
+ append_highlight_span(ret, parent.s, s, is_valid_prepostupdown(s) ? HighlightCmd : HighlightError);
+ break;
+#endif
+ case ListenPort:
+ append_highlight_span(ret, parent.s, s, is_valid_port(s) ? HighlightPort : HighlightError);
+ break;
+ case PersistentKeepalive:
+ append_highlight_span(ret, parent.s, s, is_valid_persistentkeepalive(s) ? HighlightKeepalive : HighlightError);
+ break;
+ case Endpoint: {
+ size_t colon;
+
+ if (!is_valid_endpoint(s)) {
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ break;
+ }
+ for (colon = s.len; colon --> 0;) {
+ if (s.s[colon] == ':')
+ break;
+ }
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s, colon }, HighlightHost);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon, 1 }, HighlightDelimiter);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon + 1, s.len - colon - 1 }, HighlightPort);
+ break;
+ }
+ case Address:
+ case DNS:
+ case AllowedIPs:
+ highlight_multivalue(ret, parent, s, section);
+ break;
+ default:
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ }
+}
+
+struct highlight_span *highlight_config(const char *config)
+{
+ struct highlight_span_array ret = { 0 };
+ const string_span_t s = { config, strlen(config) };
+ string_span_t current_span = { s.s, 0 };
+ enum field current_section = Invalid, current_field = Invalid;
+ enum { OnNone, OnKey, OnValue, OnComment, OnSection } state = OnNone;
+ size_t len_at_last_space = 0, equals_location = 0;
+
+ for (size_t i = 0; i <= s.len; ++i) {
+ if (i == s.len || s.s[i] == '\n' || (state != OnComment && s.s[i] == '#')) {
+ if (state == OnKey) {
+ current_span.len = len_at_last_space;
+ append_highlight_span(&ret, s.s, current_span, HighlightError);
+ } else if (state == OnValue) {
+ if (current_span.len) {
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightDelimiter);
+ current_span.len = len_at_last_space;
+ highlight_value(&ret, s, current_span, current_field);
+ } else {
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightError);
+ }
+ } else if (state == OnSection) {
+ current_span.len = len_at_last_space;
+ current_section = get_sectiontype(current_span);
+ append_highlight_span(&ret, s.s, current_span, current_section == Invalid ? HighlightError : HighlightSection);
+ } else if (state == OnComment) {
+ append_highlight_span(&ret, s.s, current_span, HighlightComment);
+ }
+ if (i == s.len)
+ break;
+ len_at_last_space = 0;
+ current_field = Invalid;
+ if (s.s[i] == '#') {
+ current_span = (string_span_t){ s.s + i, 1 };
+ state = OnComment;
+ } else {
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ state = OnNone;
+ }
+ } else if (state == OnComment) {
+ ++current_span.len;
+ } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+ if (&s.s[i] == current_span.s && !current_span.len)
+ ++current_span.s;
+ else
+ ++current_span.len;
+ } else if (s.s[i] == '=' && state == OnKey) {
+ current_span.len = len_at_last_space;
+ current_field = get_field(current_span);
+ enum field section = section_for_field(current_field);
+ if (section == Invalid || current_field == Invalid || section != current_section)
+ append_highlight_span(&ret, s.s, current_span, HighlightError);
+ else
+ append_highlight_span(&ret, s.s, current_span, HighlightField);
+ equals_location = i;
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ state = OnValue;
+ } else {
+ if (state == OnNone)
+ state = s.s[i] == '[' ? OnSection : OnKey;
+ len_at_last_space = ++current_span.len;
+ }
+ }
+
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s, -1 }, HighlightEnd);
+ return ret.spans;
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/highlighter.h b/Sources/WireGuardApp/UI/macOS/View/highlighter.h
new file mode 100644
index 0000000..8b86acb
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/highlighter.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <sys/types.h>
+#define MOBILE_WGQUICK_SUBSET
+
+enum highlight_type {
+ HighlightSection,
+ HighlightField,
+ HighlightPrivateKey,
+ HighlightPublicKey,
+ HighlightPresharedKey,
+ HighlightIP,
+ HighlightCidr,
+ HighlightHost,
+ HighlightPort,
+ HighlightMTU,
+ HighlightKeepalive,
+ HighlightComment,
+ HighlightDelimiter,
+#ifndef MOBILE_WGQUICK_SUBSET
+ HighlightTable,
+ HighlightFwMark,
+ HighlightSaveConfig,
+ HighlightCmd,
+#endif
+ HighlightError,
+ HighlightEnd
+};
+
+struct highlight_span {
+ enum highlight_type type;
+ size_t start, len;
+};
+
+struct highlight_span *highlight_config(const char *config);