]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
Added syntax highlighting conf textview
authorEric Kuck <eric@bluelinelabs.com>
Mon, 7 Jan 2019 12:47:27 +0000 (14:47 +0200)
committerRoopesh Chander <roop@roopc.net>
Mon, 14 Jan 2019 09:22:34 +0000 (14:52 +0530)
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
WireGuard/WireGuard.xcodeproj/project.pbxproj
WireGuard/WireGuard/UI/macOS/NSColor+Hex.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/View/ConfTextStorage.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/View/ConfTextView.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift
WireGuard/WireGuard/UI/macOS/highlighter.c [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/highlighter.h [new file with mode: 0644]
WireGuard/WireGuard/WireGuard-Bridging-Header.h

index 402fb297692381e6404f63b1dd53fab79f4066bb..44785c024756423cb19caeca5958ec0f08913dd0 100644 (file)
                5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A521C4449E00994C13 /* ButtonCell.swift */; };
                5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; };
                5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
+               5F52D0BB21E3781B00283CEA /* ConfTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F52D0BA21E3781B00283CEA /* ConfTextView.swift */; };
+               5F52D0BD21E3785C00283CEA /* ConfTextStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F52D0BC21E3785C00283CEA /* ConfTextStorage.swift */; };
+               5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F52D0BE21E3788900283CEA /* NSColor+Hex.swift */; };
+               5F52D0C221E378C000283CEA /* highlighter.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F52D0C121E378C000283CEA /* highlighter.c */; };
                5F9696AA21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
                5F9696AB21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
                5F9696AE21CD6F72008063FE /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
                5F4541A521C4449E00994C13 /* ButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCell.swift; sourceTree = "<group>"; };
                5F4541A821C451D100994C13 /* TunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatus.swift; sourceTree = "<group>"; };
                5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ArrayConversion.swift"; sourceTree = "<group>"; };
+               5F52D0BA21E3781B00283CEA /* ConfTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfTextView.swift; sourceTree = "<group>"; };
+               5F52D0BC21E3785C00283CEA /* ConfTextStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfTextStorage.swift; sourceTree = "<group>"; };
+               5F52D0BE21E3788900283CEA /* NSColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Hex.swift"; sourceTree = "<group>"; };
+               5F52D0C021E378C000283CEA /* highlighter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = highlighter.h; sourceTree = "<group>"; };
+               5F52D0C121E378C000283CEA /* highlighter.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = highlighter.c; sourceTree = "<group>"; };
                5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyConfigMigration.swift; sourceTree = "<group>"; };
                5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+WgQuickConfig.swift"; sourceTree = "<group>"; };
                5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceConfiguration.swift; sourceTree = "<group>"; };
                        children = (
                                6F4DD16A21DA558800690EAE /* TunnelListRow.swift */,
                                6F613D9A21DE33B8004B217A /* KeyValueRow.swift */,
+                               5F52D0BA21E3781B00283CEA /* ConfTextView.swift */,
+                               5F52D0BC21E3785C00283CEA /* ConfTextStorage.swift */,
                        );
                        path = View;
                        sourceTree = "<group>";
                                6FB1BD6621D2607E00A991BF /* Info.plist */,
                                6FB1BD6721D2607E00A991BF /* WireGuard.entitlements */,
                                6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */,
+                               5F52D0BE21E3788900283CEA /* NSColor+Hex.swift */,
+                               5F52D0C021E378C000283CEA /* highlighter.h */,
+                               5F52D0C121E378C000283CEA /* highlighter.c */,
                        );
                        path = macOS;
                        sourceTree = "<group>";
                                6FB1BDD621D50F5300A991BF /* zip.c in Sources */,
                                6FDB3C3B21DCF47400A0C0BF /* TunnelDetailTableViewController.swift in Sources */,
                                6FB1BDD721D50F5300A991BF /* WireGuardAppError.swift in Sources */,
+                               5F52D0BD21E3785C00283CEA /* ConfTextStorage.swift in Sources */,
+                               5F52D0C221E378C000283CEA /* highlighter.c in Sources */,
                                6F4DD16E21DBEA0700690EAE /* ManageTunnelsRootViewController.swift in Sources */,
                                6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */,
                                6FB1BDD821D50F5300A991BF /* WireGuardResult.swift in Sources */,
                                6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */,
                                6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */,
                                6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */,
+                               5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */,
                                6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */,
                                6FB1BDBF21D50F0200A991BF /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
                                6FB1BDC021D50F0200A991BF /* NETunnelProviderProtocol+Extension.swift in Sources */,
                                6FBA101821D656000051C35F /* StatusMenu.swift in Sources */,
                                6F613D9B21DE33B8004B217A /* KeyValueRow.swift in Sources */,
                                6FB1BDC121D50F0200A991BF /* String+ArrayConversion.swift in Sources */,
+                               5F52D0BB21E3781B00283CEA /* ConfTextView.swift in Sources */,
                                6FB1BDC221D50F0300A991BF /* LegacyConfigMigration.swift in Sources */,
                                6FBA104021D6B7040051C35F /* ErrorPresenterProtocol.swift in Sources */,
                                6FCD99AA21E0E14700BA4C82 /* NoTunnelsDetailViewController.swift in Sources */,
diff --git a/WireGuard/WireGuard/UI/macOS/NSColor+Hex.swift b/WireGuard/WireGuard/UI/macOS/NSColor+Hex.swift
new file mode 100644 (file)
index 0000000..abd5723
--- /dev/null
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import AppKit
+
+extension NSColor {
+
+    convenience init(hex: String) {
+        var hexString = hex.uppercased()
+
+        if hexString.hasPrefix("#") {
+            hexString.remove(at: hexString.startIndex)
+        }
+
+        if hexString.count != 6 {
+            fatalError("Invalid hex string \(hex)")
+        }
+
+        var rgb: UInt32 = 0
+        Scanner(string: hexString).scanHexInt32(&rgb)
+
+        self.init(red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0, green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0, blue: CGFloat(rgb & 0x0000FF) / 255.0, alpha: 1)
+    }
+
+}
diff --git a/WireGuard/WireGuard/UI/macOS/View/ConfTextStorage.swift b/WireGuard/WireGuard/UI/macOS/View/ConfTextStorage.swift
new file mode 100644 (file)
index 0000000..2db3d87
--- /dev/null
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ConfTextStorage: NSTextStorage {
+
+    struct TextColorTheme {
+        let black: NSColor
+        let red: NSColor
+        let green: NSColor
+        let yellow: NSColor
+        let blue: NSColor
+        let magenta: NSColor
+        let cyan: NSColor
+        let white: NSColor
+        let `default`: NSColor
+    }
+
+    let defaultFont = NSFont.systemFont(ofSize: 16)
+    private let boldFont = NSFont.boldSystemFont(ofSize: 16)
+
+    private var defaultAttributes: [NSAttributedString.Key: Any]! //swiftlint:disable:this implicitly_unwrapped_optional
+    private var highlightAttributes: [UInt32: [NSAttributedString.Key: Any]]! //swiftlint:disable:this implicitly_unwrapped_optional
+
+    private let backingStore: NSMutableAttributedString
+    private(set) var hasError = 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")
+    }
+
+    //swiftlint:disable:next function_body_length
+    func updateAttributes(for theme: TextColorTheme) {
+        self.defaultAttributes = [
+            .foregroundColor: theme.default,
+            .font: defaultFont
+        ]
+
+        self.highlightAttributes = [
+            HighlightSection.rawValue: [
+                .foregroundColor: theme.black,
+                .font: boldFont
+            ],
+            HighlightKeytype.rawValue: [
+                .foregroundColor: theme.blue,
+                .font: boldFont
+            ],
+            HighlightKey.rawValue: [
+                .foregroundColor: theme.yellow,
+                .font: boldFont
+            ],
+            HighlightCmd.rawValue: [
+                .foregroundColor: theme.white,
+                .font: defaultFont
+            ],
+            HighlightIP.rawValue: [
+                .foregroundColor: theme.green,
+                .font: defaultFont
+            ],
+            HighlightCidr.rawValue: [
+                .foregroundColor: theme.yellow,
+                .font: defaultFont
+            ],
+            HighlightHost.rawValue: [
+                .foregroundColor: theme.green,
+                .font: boldFont
+            ],
+            HighlightPort.rawValue: [
+                .foregroundColor: theme.magenta,
+                .font: defaultFont
+            ],
+            HighlightTable.rawValue: [
+                .foregroundColor: theme.blue,
+                .font: defaultFont
+            ],
+            HighlightFwMark.rawValue: [
+                .foregroundColor: theme.blue,
+                .font: defaultFont
+            ],
+            HighlightMTU.rawValue: [
+                .foregroundColor: theme.blue,
+                .font: defaultFont
+            ],
+            HighlightSaveConfig.rawValue: [
+                .foregroundColor: theme.blue,
+                .font: defaultFont
+            ],
+            HighlightKeepalive.rawValue: [
+                .foregroundColor: theme.blue,
+                .font: defaultFont
+            ],
+            HighlightComment.rawValue: [
+                .foregroundColor: theme.cyan,
+                .font: defaultFont
+            ],
+            HighlightDelimiter.rawValue: [
+                .foregroundColor: theme.cyan,
+                .font: defaultFont
+            ],
+            HighlightError.rawValue: [
+                .foregroundColor: theme.red,
+                .font: defaultFont,
+                .underlineStyle: 1
+            ]
+        ]
+
+        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.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 highlightSyntax() {
+        hasError = false
+
+        backingStore.beginEditing()
+        var spans = highlight_config(backingStore.string.cString(using: String.Encoding.utf8))!
+
+        while spans.pointee.type != HighlightEnd {
+            let span = spans.pointee
+
+            let attributes = self.highlightAttributes[span.type.rawValue] ?? defaultAttributes
+            backingStore.setAttributes(attributes, range: NSRange(location: span.start, length: span.len))
+
+            if span.type == HighlightError {
+                hasError = true
+            }
+
+            spans = spans.successor()
+        }
+        backingStore.endEditing()
+
+        beginEditing()
+        edited(.editedAttributes, range: NSRange(location: 0, length: (backingStore.string as NSString).length), changeInLength: 0)
+        endEditing()
+    }
+
+}
diff --git a/WireGuard/WireGuard/UI/macOS/View/ConfTextView.swift b/WireGuard/WireGuard/UI/macOS/View/ConfTextView.swift
new file mode 100644 (file)
index 0000000..8526e6c
--- /dev/null
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ConfTextView: NSTextView {
+
+    private let confTextStorage = ConfTextStorage()
+
+    var hasError: Bool { return confTextStorage.hasError }
+
+    override var string: String {
+        didSet {
+            confTextStorage.highlightSyntax()
+        }
+    }
+
+    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
+        updateTheme()
+        delegate = self
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func viewDidChangeEffectiveAppearance() {
+        updateTheme()
+    }
+
+    private func updateTheme() {
+        let theme: ConfTextStorage.TextColorTheme
+        switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) ?? .aqua {
+        case .darkAqua:
+            theme = ConfTextStorage.TextColorTheme(black: NSColor(hex: "#c7c7c7"), red: NSColor(hex: "#dc322f"), green: NSColor(hex: "#859900"), yellow: NSColor(hex: "#c7c400"), blue: NSColor(hex: "#268bd2"), magenta: NSColor(hex: "#d33682"), cyan: NSColor(hex: "#2aa198"), white: NSColor(hex: "#383838"), default: NSColor(hex: "#c7c7c7"))
+        default:
+            theme = ConfTextStorage.TextColorTheme(black: NSColor(hex: "#000000"), red: NSColor(hex: "#c91b00"), green: NSColor(hex: "#00c200"), yellow: NSColor(hex: "#c7c400"), blue: NSColor(hex: "#0225c7"), magenta: NSColor(hex: "#c930c7"), cyan: NSColor(hex: "#00c5c7"), white: NSColor(hex: "#c7c7c7"), default: NSColor(hex: "#000000"))
+        }
+        confTextStorage.updateAttributes(for: theme)
+    }
+
+}
+
+extension ConfTextView: NSTextViewDelegate {
+
+    func textDidChange(_ notification: Notification) {
+        confTextStorage.highlightSyntax()
+        needsDisplay = true
+    }
+
+}
index 52b80f69a815fa427eeeecab96ef8963743f970b..47f488c33faf784ab7824bdb65ebcbc297112d6e 100644 (file)
@@ -18,12 +18,12 @@ class TunnelEditViewController: NSViewController {
     }()
 
     let textView: NSTextView = {
-        let textView = NSTextView()
+        let textView = ConfTextView()
         let minWidth: CGFloat = 120
         let minHeight: CGFloat = 60
         textView.minSize = NSSize(width: 0, height: minHeight)
         textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
-        textView.autoresizingMask = .width
+        textView.autoresizingMask = [.width, .height]
         textView.isHorizontallyResizable = true
         if let textContainer = textView.textContainer {
             textContainer.size = NSSize(width: minWidth, height: CGFloat.greatestFiniteMagnitude)
diff --git a/WireGuard/WireGuard/UI/macOS/highlighter.c b/WireGuard/WireGuard/UI/macOS/highlighter.c
new file mode 100644 (file)
index 0000000..6edf575
--- /dev/null
@@ -0,0 +1,588 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <string.h>
+#include <strings.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <errno.h>
+#include "highlighter.h"
+
+typedef struct {
+    const char *s;
+    size_t len;
+} string_span_t;
+
+static bool is_valid_key(string_span_t s)
+{
+    if (s.len != 44 || s.s[43] != '=')
+        return false;
+    
+    for (size_t i = 0; i < 43; ++i) {
+        if (!((s.s[i] >= '/' && s.s[i] <= '9') ||
+              (s.s[i] >= 'A' && s.s[i] <= 'Z') ||
+              (s.s[i] >= 'a' && s.s[i] <= 'z') ||
+              s.s[i] == '+'))
+            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 (isdigit(s.s[i])) {
+            ++num_digit;
+            continue;
+        }
+        if (s.s[i] == '.') {
+            --num_entity;
+            continue;
+        }
+        
+        if (!((s.s[i] >= 'A' && s.s[i] <= 'Z') ||
+              (s.s[i] >= 'a' && s.s[i] <= 'z') ||
+              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 && isdigit(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 && isxdigit(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_u16(string_span_t s)
+{
+    uint32_t val = 0;
+    
+    if (s.len > 5 || !s.len)
+        return false;
+    
+    for (size_t i = 0; i < s.len; ++i) {
+        if (!isdigit(s.s[i]))
+            return false;
+        val = 10 * val + s.s[i] - '0';
+    }
+    return val <= 65535;
+}
+
+static bool is_valid_port(string_span_t s)
+{
+    return is_valid_u16(s);
+}
+
+static bool is_valid_mtu(string_span_t s)
+{
+    return is_valid_u16(s);
+}
+
+static bool is_valid_persistentkeepalive(string_span_t s)
+{
+    if (s.len == 3 && !memcmp(s.s, "off", 3))
+        return true;
+    return is_valid_u16(s);
+}
+
+static bool is_valid_u32(string_span_t s)
+{
+    uint64_t val = 0;
+    
+    if (s.len > 10 || !s.len)
+        return false;
+    
+    if (s.len > 2 && s.s[0] == '0' && s.s[1] == 'x') {
+        for (size_t i = 2; i < s.len; ++i) {
+            if (s.s[i] - '0' < 10)
+                val = 16 * val + (s.s[i] - '0');
+            else if ((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 (!isdigit(s.s[i]))
+                return false;
+            val = 10 * val + s.s[i] - '0';
+        }
+    }
+    return val <= 4294967295U;
+}
+
+static bool is_valid_fwmark(string_span_t s)
+{
+    if (s.len == 3 && !memcmp(s.s, "off", 3))
+        return true;
+    return is_valid_u32(s);
+}
+
+static bool is_valid_table(string_span_t s)
+{
+    if (s.len == 4 && !memcmp(s.s, "auto", 3))
+        return true;
+    if (s.len == 3 && !memcmp(s.s, "off", 3))
+        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_u32(s);
+}
+
+static bool is_valid_saveconfig(string_span_t s)
+{
+    return (s.len == 4 && !memcmp(s.s, "true", 4)) ||
+    (s.len == 5 && !memcmp(s.s, "false", 5));
+}
+
+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 (!((s.s[i] >= 'A' && s.s[i] <= 'Z') ||
+              (s.s[i] >= 'a' && s.s[i] <= 'z') ||
+              isdigit(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 (!isdigit(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);
+}
+
+static bool is_valid_dns(string_span_t s)
+{
+    return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+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;
+}
+
+enum keytype {
+    InterfaceSection,
+    PrivateKey,
+    ListenPort,
+    FwMark,
+    Address,
+    DNS,
+    MTU,
+    Table,
+    PreUp, PostUp, PreDown, PostDown,
+    SaveConfig,
+    
+    PeerSection,
+    PublicKey,
+    PresharedKey,
+    AllowedIPs,
+    Endpoint,
+    PersistentKeepalive,
+    
+    Invalid
+};
+
+static enum keytype section_for_keytype(enum keytype t)
+{
+    if (t > InterfaceSection && t < PeerSection)
+        return InterfaceSection;
+    if (t > PeerSection && t < Invalid)
+        return PeerSection;
+    return Invalid;
+}
+
+static enum keytype get_keytype(string_span_t s)
+{
+#define check_enum(t) do { if (s.len == strlen(#t) && !strncasecmp(#t, s.s, s.len)) return t; } while (0)
+    check_enum(PrivateKey);
+    check_enum(ListenPort);
+    check_enum(FwMark);
+    check_enum(Address);
+    check_enum(DNS);
+    check_enum(MTU);
+    check_enum(Table);
+    check_enum(PreUp);
+    check_enum(PostUp);
+    check_enum(PreDown);
+    check_enum(PostDown);
+    check_enum(PublicKey);
+    check_enum(PresharedKey);
+    check_enum(AllowedIPs);
+    check_enum(Endpoint);
+    check_enum(PersistentKeepalive);
+    check_enum(SaveConfig);
+    return Invalid;
+#undef check_enum
+}
+
+static enum keytype get_sectiontype(string_span_t s)
+{
+    if (s.len == 6 && !strncasecmp("[Peer]", s.s, 6))
+        return PeerSection;
+    if (s.len == 11 && !strncasecmp("[Interface]", s.s, 11))
+        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 keytype section)
+{
+    switch (section) {
+        case DNS:
+            append_highlight_span(ret, parent.s, s, is_valid_dns(s) ? HighlightIP : 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 keytype 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 keytype section)
+{
+    switch (section) {
+        case PrivateKey:
+        case PublicKey:
+        case PresharedKey:
+            append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightKey : 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 MTU:
+            append_highlight_span(ret, parent.s, s, is_valid_mtu(s) ? HighlightMTU : HighlightError);
+            break;
+        case SaveConfig:
+            append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? HighlightSaveConfig : HighlightError);
+            break;
+        case PreUp:
+        case PostUp:
+        case PreDown:
+        case PostDown:
+            append_highlight_span(ret, parent.s, s, is_valid_prepostupdown(s) ? HighlightCmd : HighlightError);
+            break;
+        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 keytype current_section = Invalid, current_keytype = 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_keytype);
+                } 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_keytype = 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_keytype = get_keytype(current_span);
+            enum keytype section = section_for_keytype(current_keytype);
+            if (section == Invalid || current_keytype == Invalid || section != current_section)
+                append_highlight_span(&ret, s.s, current_span, HighlightError);
+            else
+                append_highlight_span(&ret, s.s, current_span, HighlightKeytype);
+            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/WireGuard/WireGuard/UI/macOS/highlighter.h b/WireGuard/WireGuard/UI/macOS/highlighter.h
new file mode 100644 (file)
index 0000000..9368af6
--- /dev/null
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <sys/types.h>
+
+enum highlight_type {
+    HighlightSection,
+    HighlightKeytype,
+    HighlightKey,
+    HighlightCmd,
+    HighlightIP,
+    HighlightCidr,
+    HighlightHost,
+    HighlightPort,
+    HighlightTable,
+    HighlightFwMark,
+    HighlightMTU,
+    HighlightSaveConfig,
+    HighlightKeepalive,
+    HighlightComment,
+    HighlightDelimiter,
+    HighlightError,
+    HighlightEnd
+};
+
+struct highlight_span {
+    enum highlight_type type;
+    size_t start, len;
+};
+
+struct highlight_span *highlight_config(const char *config);
index 21cd2a2d129113c6deae55e33b865a7abd95c957..95e712b363c0a9f2379a0e69b39f9bf597c78fd3 100644 (file)
@@ -3,3 +3,4 @@
 #include "zip.h"
 #include "wireguard-go-version.h"
 #include "ringlogger.h"
+#include "highlighter.h"